-
-}
diff --git a/assets/javascripts/discourse/connectors/search-menu-results-top/ai-quick-search-info.gjs b/assets/javascripts/discourse/connectors/search-menu-results-top/ai-quick-search-info.gjs
deleted file mode 100644
index 36cc54f6..00000000
--- a/assets/javascripts/discourse/connectors/search-menu-results-top/ai-quick-search-info.gjs
+++ /dev/null
@@ -1,31 +0,0 @@
-import Component from "@glimmer/component";
-import { service } from "@ember/service";
-import { isValidSearchTerm } from "discourse/lib/search";
-import { i18n } from "discourse-i18n";
-
-export default class AiQuickSearchInfo extends Component {
- @service search;
- @service siteSettings;
- @service quickSearch;
-
- get termTooShort() {
- // We check the validity again here because the input may have changed
- // since the last time we checked, so we may want to stop showing the error
- const validity = !isValidSearchTerm(
- this.search.activeGlobalSearchTerm,
- this.siteSettings
- );
-
- return (
- validity &&
- this.quickSearch.invalidTerm &&
- this.search.activeGlobalSearchTerm?.length > 0
- );
- }
-
-
- {{#if this.termTooShort}}
-
-;
-
-export default {
- name: "discourse-ai-related-topics",
-
- initialize(container) {
- const settings = container.lookup("service:site-settings");
-
- if (
- !settings.ai_embeddings_enabled ||
- !settings.ai_embeddings_semantic_related_topics_enabled
- ) {
- return;
- }
-
- withPluginApi("1.37.2", (api) => {
- api.registerMoreTopicsTab({
- id: "related-topics",
- name: i18n("discourse_ai.related_topics.pill"),
- icon: "discourse-sparkles",
- component: RelatedTopics,
- condition: ({ topic }) => topic.relatedTopics?.length,
- });
-
- api.modifyClass(
- "model:topic",
- (Superclass) =>
- class extends Superclass {
- @tracked related_topics;
- relatedTopicsCache = [];
-
- @cached
- get relatedTopics() {
- // Used to keep related topics when a user scrolls up from the
- // bottom of the topic and then scrolls back down
- if (this.related_topics) {
- this.relatedTopicsCache = this.related_topics;
- }
- return this.relatedTopicsCache?.map((topic) =>
- this.store.createRecord("topic", topic)
- );
- }
- }
- );
-
- api.modifyClass(
- "model:post-stream",
- (Superclass) =>
- class extends Superclass {
- _setSuggestedTopics(result) {
- super._setSuggestedTopics(...arguments);
- this.topic.related_topics = result.related_topics;
- }
- }
- );
- });
- },
-};
diff --git a/assets/javascripts/initializers/translation.js b/assets/javascripts/initializers/translation.js
deleted file mode 100644
index d327ff68..00000000
--- a/assets/javascripts/initializers/translation.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { apiInitializer } from "discourse/lib/api";
-import cookie from "discourse/lib/cookie";
-
-export default apiInitializer((api) => {
- const settings = api.container.lookup("service:site-settings");
-
- if (!settings.discourse_ai_enabled || !settings.ai_translation_enabled) {
- return;
- }
-
- api.registerCustomPostMessageCallback(
- "localized",
- (topicController, data) => {
- if (!cookie("content-localization-show-original")) {
- const postStream = topicController.get("model.postStream");
- postStream.triggerChangedPost(data.id, data.updated_at).then(() => {
- topicController.appEvents.trigger("post-stream:refresh", {
- id: data.id,
- });
- });
- }
- }
- );
-});
diff --git a/assets/javascripts/lib/discourse-markdown/ai-tags.js b/assets/javascripts/lib/discourse-markdown/ai-tags.js
deleted file mode 100644
index 492e5540..00000000
--- a/assets/javascripts/lib/discourse-markdown/ai-tags.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export function setup(helper) {
- helper.allowList(["details[class=ai-quote]"]);
- helper.allowList([
- "div[class=ai-artifact]",
- "div[data-ai-artifact-id]",
- "div[data-ai-artifact-version]",
- "div[data-ai-artifact-autorun]",
- "div[data-ai-artifact-height]",
- "div[data-ai-artifact-width]",
- ]);
-}
diff --git a/assets/stylesheets/common/ai-blinking-animation.scss b/assets/stylesheets/common/ai-blinking-animation.scss
deleted file mode 100644
index b43ce99e..00000000
--- a/assets/stylesheets/common/ai-blinking-animation.scss
+++ /dev/null
@@ -1,139 +0,0 @@
-.ai-blinking-animation {
- list-style: none;
- display: flex;
- flex-wrap: wrap;
- padding: 0;
- margin: 0;
-
- &__list-item {
- background: var(--primary-300);
- border-radius: var(--d-border-radius);
- margin-right: 8px;
- margin-bottom: 8px;
- height: 1em;
- opacity: 0;
- display: block;
-
- &:nth-child(1) {
- width: 10%;
- }
-
- &:nth-child(2) {
- width: 12%;
- }
-
- &:nth-child(3) {
- width: 18%;
- }
-
- &:nth-child(4) {
- width: 14%;
- }
-
- &:nth-child(5) {
- width: 18%;
- }
-
- &:nth-child(6) {
- width: 14%;
- }
-
- &:nth-child(7) {
- width: 22%;
- }
-
- &:nth-child(8) {
- width: 5%;
- }
-
- &:nth-child(9) {
- width: 25%;
- }
-
- &:nth-child(10) {
- width: 14%;
- }
-
- &:nth-child(11) {
- width: 18%;
- }
-
- &:nth-child(12) {
- width: 12%;
- }
-
- &:nth-child(13) {
- width: 22%;
- }
-
- &:nth-child(14) {
- width: 18%;
- }
-
- &:nth-child(15) {
- width: 13%;
- }
-
- &:nth-child(16) {
- width: 22%;
- }
-
- &:nth-child(17) {
- width: 19%;
- }
-
- &:nth-child(18) {
- width: 13%;
- }
-
- &:nth-child(19) {
- width: 22%;
- }
-
- &:nth-child(20) {
- width: 25%;
- }
-
- &.is-shown {
- opacity: 1;
- }
-
- &.show {
- animation: appear 0.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) 0s forwards;
-
- @media (prefers-reduced-motion) {
- animation-duration: 0s;
- }
- }
-
- @media (prefers-reduced-motion: no-preference) {
- &.blink {
- animation: blink 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
- }
- }
- }
-
- @keyframes appear {
- 0% {
- opacity: 0;
- }
-
- 100% {
- opacity: 1;
- }
- }
-
- @keyframes blink {
- 0% {
- opacity: 1;
- }
-
- 50% {
- opacity: 0.5;
- }
-
- 100% {
- opacity: 1;
- }
- }
-}
diff --git a/assets/stylesheets/common/ai-features.scss b/assets/stylesheets/common/ai-features.scss
deleted file mode 100644
index a97243a7..00000000
--- a/assets/stylesheets/common/ai-features.scss
+++ /dev/null
@@ -1,160 +0,0 @@
-.ai-features-list {
- margin-block: 2rem;
- display: flex;
- flex-direction: column;
- gap: 3rem;
-}
-
-.ai-module {
- &__header {
- border-bottom: 1px solid var(--primary-low);
- padding-bottom: var(--space-2);
- }
-
- &__module-title {
- display: flex;
- justify-content: space-between;
- }
-}
-
-.ai-feature-cards {
- gap: var(--space-4);
-}
-
-.ai-feature-card {
- background: var(--secondary);
- border: 1px solid var(--primary-low);
- border-radius: var(--d-border-radius);
- padding: var(--space-3) var(--space-4) var(--space-2);
- display: flex;
- flex-direction: column;
-
- &.admin-section-landing-item {
- margin: 0;
- }
-
- &__feature-name {
- margin-bottom: var(--space-2);
- }
-
- &__label {
- margin: var(--space-1) var(--space-1) 0 0;
- }
-
- &__llm,
- &__persona,
- &__groups {
- font-size: var(--font-down-1-rem);
- display: flex;
- align-items: baseline;
- flex-flow: row wrap;
- color: var(--primary-high);
- }
-
- &__persona {
- @include ellipsis;
- }
-
- &__persona-button,
- &__llm-button {
- padding: 0;
- margin-right: var(--space-1);
- overflow: hidden;
-
- .d-button-label {
- min-height: 1.5em;
-
- @include ellipsis;
- }
- }
-
- &__groups {
- display: flex;
- flex-flow: row wrap;
- gap: var(--space-1);
- }
-
- &__item-groups {
- list-style: none;
- display: flex;
- flex-flow: row wrap;
- gap: var(--space-1);
- margin: 0;
-
- li {
- font-size: var(--font-down-1);
- border-radius: var(--d-border-radius);
- background: var(--primary-very-low);
- border: 1px solid var(--primary-low);
- padding: 1px 3px;
- }
- }
-}
-
-.ai-feature-editor {
- &__header {
- border-bottom: 1px solid var(--primary-low);
- }
-
- .setting {
- margin-block: 1.5rem;
- }
-
- .setting-label {
- font-size: var(--font-down-1-rem);
- color: var(--primary-high);
-
- a[title="View change history"],
- .history-icon {
- display: none;
- }
- }
-
- .setting-value {
- .desc {
- font-size: var(--font-down-1-rem);
- color: var(--primary-high-or-secondary-low);
- }
- }
-
- .setting-controls,
- .setting-controls__undo {
- font-size: var(--font-down-1-rem);
- margin-top: var(--space-2);
- }
-}
-
-.ai-features__controls {
- display: flex;
- gap: var(--space-2);
-
- .filter-input-container {
- flex: 6 1 auto;
- }
-
- .d-select {
- flex: 1 1 auto;
- max-width: 10em;
- }
-}
-
-.ai-features__no-results {
- display: flex;
- flex-direction: column;
- text-align: center;
- justify-content: center;
- padding: var(--space-6);
- gap: var(--space-2);
-
- h3 {
- font-weight: normal;
- }
-
- .btn {
- align-self: center;
- }
-}
-
-.ai-expanded-list__toggle-button {
- padding: 0;
-}
diff --git a/assets/stylesheets/common/ai-user-settings.scss b/assets/stylesheets/common/ai-user-settings.scss
deleted file mode 100644
index 95fd6a1e..00000000
--- a/assets/stylesheets/common/ai-user-settings.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.user-preferences .ai-user-preferences {
- legend {
- margin-bottom: 1rem;
- }
-
- .control-group {
- margin-bottom: 0;
- }
-
- .save-button {
- margin-top: 2rem;
- }
-}
diff --git a/assets/stylesheets/common/streaming.scss b/assets/stylesheets/common/streaming.scss
deleted file mode 100644
index bbeb720f..00000000
--- a/assets/stylesheets/common/streaming.scss
+++ /dev/null
@@ -1,130 +0,0 @@
-@keyframes flashing {
- 0%,
- 100% {
- opacity: 0;
- }
-
- 50% {
- opacity: 1;
- }
-}
-
-@mixin progress-dot {
- content: "\25CF";
- font-family:
- "Söhne Circle",
- system-ui,
- -apple-system,
- "Segoe UI",
- Roboto,
- Ubuntu,
- Cantarell,
- "Noto Sans",
- sans-serif;
- line-height: normal;
- margin-left: 0.25rem;
- vertical-align: baseline;
- animation: flashing 1.5s 3s infinite;
- display: inline-block;
- font-size: 1rem;
- color: var(--tertiary-medium);
-}
-
-.streamable-content.streaming .cooked p:last-child::after {
- @include progress-dot;
-}
-
-article.streaming .cooked {
- .progress-dot::after {
- @include progress-dot;
- }
-
- > .progress-dot:only-child::after {
- // if the progress dot is the only content
- // we are likely waiting longer for a response
- // so it can start animating instantly
- animation: flashing 1.5s infinite;
- }
-}
-
-@keyframes ai-indicator-wave {
- 0%,
- 60%,
- 100% {
- transform: initial;
- }
-
- 30% {
- transform: translateY(-0.2em);
- }
-}
-
-.ai-indicator-wave {
- flex: 0 0 auto;
- display: inline-flex;
-
- &__dot {
- display: inline-block;
-
- @media (prefers-reduced-motion: no-preference) {
- animation: ai-indicator-wave 1.8s linear infinite;
- }
-
- &:nth-child(2) {
- animation-delay: -1.6s;
- }
-
- &:nth-child(3) {
- animation-delay: -1.4s;
- }
- }
-}
-
-@keyframes mark-blink {
- 0%,
- 100% {
- border-color: transparent;
- }
-
- 50% {
- border-color: var(--highlight-high);
- }
-}
-
-@keyframes fade-in-highlight {
- from {
- opacity: 0.5;
- }
-
- to {
- opacity: 1;
- }
-}
-
-mark.highlight {
- background-color: var(--highlight-high);
- animation: fade-in-highlight 0.5s ease-in-out forwards;
-}
-
-.composer-ai-helper-modal__suggestion.thinking mark.highlight {
- animation: mark-blink 1s step-start 0s infinite;
- animation-name: mark-blink;
-}
-
-.composer-ai-helper-modal__loading.inline-diff {
- white-space: pre-wrap;
-}
-
-.composer-ai-helper-modal__suggestion.inline-diff {
- white-space: pre-wrap;
-
- del:last-child {
- text-decoration: none;
- background-color: transparent;
- color: var(--primary-low-mid);
- }
-
- .diff-inner {
- display: inline;
- }
-}
diff --git a/assets/stylesheets/modules/ai-bot-conversations/common.scss b/assets/stylesheets/modules/ai-bot-conversations/common.scss
deleted file mode 100644
index 27275aa4..00000000
--- a/assets/stylesheets/modules/ai-bot-conversations/common.scss
+++ /dev/null
@@ -1,517 +0,0 @@
-@use "lib/viewport";
-
-// Hide the new question button from the hamburger menu's footer on desktop
-.desktop-view .hamburger-panel .ai-new-question-button {
- display: none;
-}
-
-body.has-ai-conversations-sidebar {
- .ai-new-question-button {
- width: 100%;
-
- &__wrapper {
- background: var(--secondary);
- margin: 1.8em 1em 0;
-
- .mobile-view & {
- padding: 1em;
- position: sticky;
- top: 0;
- margin: -0.5em 0 0; // avoid shift when sticking
- z-index: 1;
- }
- }
- }
-
- // When the sidebar is empty, we want to hide the "Today" header
- // and only show the empty state component
- &.has-empty-ai-conversations-sidebar
- .sidebar-section[data-section-name="today"]
- .sidebar-section-header-wrapper {
- display: none;
- }
-
- // When the sidebar isn't empty, BUT "today" is empty, hide
- // the entire section
- &:not(.has-empty-ai-conversations-sidebar)
- .sidebar-section[data-section-name="today"]:has(
- .ai-bot-sidebar-empty-state
- ) {
- display: none;
- }
-
- .sidebar-toggle-all-sections {
- display: none;
- }
-
- // topic elements
- #topic-footer-button-share-and-invite,
- body:not(.staff) #topic-footer-button-archive,
- #topic-footer-buttons .topic-notifications-button,
- .bookmark-menu-trigger,
- .more-topics__container,
- .private-message-glyph-wrapper,
- .topic-header-participants,
- .topic-above-footer-buttons-outlet,
- #topic-footer-buttons .topic-footer-main-buttons details {
- display: none;
- }
-
- .topic-timer-info {
- border: none;
- }
-
- .topic-owner .actions .create-flag {
- // why flag my own post
- display: none;
- }
-
- .container.posts {
- margin-bottom: 0;
-
- .topic-navigation.with-timeline {
- top: calc(var(--header-offset, 60px) + 5.5em);
- }
-
- .topic-navigation {
- .topic-notifications-button {
- display: none;
- }
- }
- }
-
- #topic-title {
- display: flex;
- justify-content: center;
- width: 100%;
-
- .title-wrapper {
- width: 100%;
- max-width: 960px;
- }
- }
-
- .small-action,
- .onscreen-post .row {
- justify-content: center;
- }
-
- #topic-footer-buttons {
- margin-top: 1em;
- width: 100%;
- max-width: 50.5em;
-
- .topic-footer-main-buttons {
- justify-content: flex-end;
- }
- }
-
- #topic-progress-wrapper.docked {
- display: none;
- }
-
- @include viewport.until(lg) {
- .archetype-private_message .topic-post:last-child {
- margin-bottom: 0;
- }
- }
-
- nav.post-controls .actions button {
- padding: 0.5em 0.65em;
-
- &.reply {
- .d-icon {
- margin-right: 0.45em;
- }
- }
- }
-
- .ai-bot-conversations {
- --input-max-width: 46em;
- display: flex;
- flex-direction: column;
- height: calc(100dvh - var(--header-offset) - 5em);
-
- .persona-llm-selector {
- display: flex;
- gap: 0.5em;
- justify-content: flex-start;
-
- .select-kit-body {
- .select-kit-collection {
- max-height: 50vh;
- overflow-y: auto;
- }
- }
-
- &__selection-wrapper {
- display: flex;
- flex-direction: column;
- min-width: 0;
-
- @include viewport.until(sm) {
- .select-kit-header-wrapper {
- font-size: var(--font-down-1);
- }
- }
-
- label {
- font-size: var(--font-down-1);
- font-weight: 300;
- margin-left: 1em;
- margin-bottom: 0;
- }
-
- .name {
- display: block;
-
- @include ellipsis;
- }
- }
-
- .btn {
- display: flex;
- justify-content: flex-start;
- background-color: transparent;
- font-weight: bold;
- }
-
- .btn:hover,
- .btn:focus {
- background-color: transparent;
- color: var(--primary);
- }
-
- .btn:hover .d-icon,
- .btn:focus .d-icon {
- color: var(--primary);
- }
- }
-
- &__content-wrapper {
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- align-items: center;
- justify-content: center;
- flex: 1 1 auto;
- gap: 0.5em;
-
- .loading-container {
- display: contents;
- }
- }
-
- &__title {
- font-size: var(--font-up-5);
- font-weight: bold;
- text-align: center;
- margin-bottom: 0.25em;
- line-height: var(--line-height-medium);
-
- // optical centering for layout balance
- @media screen and (min-height: 600px) {
- margin-top: -6em;
- }
- }
-
- &__input-wrapper {
- --input-min-height: 2.5em;
- display: flex;
- align-items: end;
- width: 100%;
- border: 1px solid var(--primary-low);
- border-radius: var(--d-input-border-radius);
-
- &:has(textarea[disabled]) {
- background: var(--primary-very-low);
- }
-
- @include viewport.from(sm) {
- width: 80%;
- max-width: var(--input-max-width);
- }
-
- &:focus-within {
- border-color: var(--tertiary);
- }
-
- .ai-conversation-submit {
- .d-icon {
- color: var(--primary-medium);
- padding: 0.5em;
- }
-
- &:hover,
- &:focus-visible {
- .d-icon {
- color: var(--primary-medium);
- }
- }
- }
-
- .ai-bot-upload-btn {
- min-height: var(--input-min-height);
- border: none;
-
- .d-icon {
- background: var(--primary-low);
- padding: 0.5em;
- border-radius: 100%;
- }
-
- &:hover,
- &:focus-visible {
- .d-icon {
- color: var(--primary);
- }
- }
- }
-
- #ai-bot-conversations-input {
- --scrollbarBg: transparent;
- --scrollbarThumbBg: var(--primary-low);
- --scrollbarWidth: 10px;
- box-sizing: border-box;
- flex-grow: 1;
- margin: 0;
- resize: none;
- max-height: 30vh;
- min-height: var(--input-min-height);
- border-radius: 0 var(--d-button-border-radius)
- var(--d-button-border-radius) 0;
- border: none;
- padding-block: 0.8em;
- padding-inline: 0;
- scrollbar-color: var(--scrollbarThumbBg) var(--scrollbarBg);
- scrollbar-width: thin;
- transition: scrollbar-color 0.25s ease-in-out;
- height: 100%;
- line-height: var(--line-height-large);
-
- &::-webkit-scrollbar-thumb {
- background-color: var(--scrollbarThumbBg);
- border-radius: calc(var(--scrollbarWidth) / 2);
- border: calc(var(--scrollbarWidth) / 4) solid var(--secondary);
- }
-
- &::-webkit-scrollbar-track {
- background-color: transparent;
- }
-
- &::-webkit-scrollbar {
- width: var(--scrollbarWidth);
- }
-
- &::-moz-scrollbar-thumb {
- background-color: var(--scrollbarThumbBg);
- border-radius: calc(var(--scrollbarWidth) / 2);
- border: calc(var(--scrollbarWidth) / 4) solid var(--secondary);
- }
-
- &::-moz-scrollbar-track {
- background-color: transparent;
- }
-
- &::-moz-scrollbar {
- width: var(--scrollbarWidth);
- }
-
- &:focus-visible {
- outline: none;
- border-color: var(--tertiary);
- }
-
- &:not(:placeholder-shown) + .ai-conversation-submit {
- will-change: scale;
-
- &:hover,
- &:focus-visible {
- transform: scale(1.2);
- }
-
- .d-icon {
- color: var(--tertiary);
- }
- }
- }
- }
-
- .ai-disclaimer {
- text-align: center;
- font-size: var(--font-down-1);
- color: var(--primary-700);
- margin: 0;
-
- @include viewport.from(sm) {
- width: 80%;
- max-width: var(--input-max-width);
- }
- }
-
- .sidebar-footer-wrapper {
- display: flex;
-
- .powered-by-discourse {
- display: block;
- }
-
- button {
- display: none;
- }
- }
-
- .topic-footer-main-buttons {
- justify-content: flex-end;
- }
-
- .ai-bot-conversations__uploads-container {
- width: 100%;
- display: flex;
- flex-wrap: wrap;
- gap: 0.5em;
-
- @include viewport.from(sm) {
- width: 80%;
- max-width: var(--input-max-width);
- }
- }
-
- .ai-bot-upload {
- display: flex;
- align-items: center;
- border: 1px solid var(--primary-low);
- border-radius: 10em;
- padding-left: 0.75em;
- color: var(--primary-high);
- font-size: var(--font-down-2);
-
- &__progress {
- margin-left: 0.5em;
- }
-
- &:hover,
- &:focus-visible {
- .d-icon {
- color: var(--danger);
- }
- }
- }
- }
-
- @include viewport.until(sm) {
- .share-ai-conversation-button {
- .d-icon {
- margin: 0;
- }
-
- .d-button-label {
- display: none;
- }
- }
- }
-
- // custom user card link
- .user-card-meta__profile-link {
- display: block;
- padding: 0.5em 0 0.25em;
-
- .d-icon {
- font-size: var(--font-down-1);
- margin-right: 0.15em;
- }
- }
-
- // hide extra buttons
- .timeline-container .topic-timeline .timeline-footer-controls {
- display: none;
- }
-
- .topic-footer-main-buttons {
- button:not(
- .create,
- .share-ai-conversation-button,
- .topic-admin-menu-trigger
- ) {
- display: none;
- }
- }
-
- .topic-map {
- box-sizing: border-box;
- width: 100%;
- margin: 0 auto;
- padding-block: 0.5em;
-
- .topic-map__views-trigger,
- .topic-map__likes-trigger,
- .summarization-button,
- &__private-message-map,
- .topic-map__users-list {
- display: none;
- }
-
- &.--bottom {
- padding-top: 0;
- }
-
- section {
- background: transparent;
- border-block: 1px solid var(--primary-low);
- }
-
- &__contents {
- padding: 0.5em 1.25em;
-
- @include viewport.from(sm) {
- padding: 0.5em 0.5em 0.5em 1.9em;
- }
- }
-
- &__stats {
- height: 100%;
- flex-wrap: nowrap;
- gap: 1em;
- }
-
- .ai-conversation__participants {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5em 0.25em;
- align-items: center;
-
- .avatar {
- width: 2em;
- height: 2em;
- }
-
- .trigger-group-card {
- display: flex;
- align-items: center;
- border: 1px solid var(--primary-low);
- border-radius: var(--d-button-border-radius);
- padding: 0.25em 0.5em;
- font-size: var(--font-down-1);
-
- span {
- position: relative;
- top: -1px;
- }
-
- .d-icon {
- position: relative;
- top: 1px;
- }
-
- a {
- color: var(--primary-medium);
- }
- }
-
- .btn {
- font-size: var(--font-down-1);
- margin-right: 0.5em;
- }
- }
- }
-}
diff --git a/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss b/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss
deleted file mode 100644
index 8867c45f..00000000
--- a/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss
+++ /dev/null
@@ -1,151 +0,0 @@
-@keyframes artifact-remove {
- to {
- height: 0;
- overflow: hidden;
- }
-}
-
-@keyframes artifact-fade {
- to {
- opacity: 0;
- }
-}
-
-.ai-artifact__wrapper {
- height: 500px;
- padding-bottom: 2em;
-
- iframe {
- width: 100%;
- height: calc(100% - 2em);
- }
-
- &.ai-artifact__seamless {
- padding-bottom: 1em;
-
- iframe {
- height: 100%;
- }
- }
-
- &:not(.ai-artifact__expanded, .ai-artifact__seamless) {
- iframe {
- box-shadow: var(--shadow-card);
- }
- }
-}
-
-.ai-artifact__click-to-run {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100%;
- background: var(--primary-very-low);
-}
-
-.ai-artifact__panel {
- display: none;
-}
-
-html.ai-artifact-expanded {
- overflow: hidden;
-}
-
-.ai-artifact__footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- .ai-artifact__expand-button {
- margin-left: auto;
- padding-right: 0;
-
- .d-icon {
- font-size: var(--font-down-1);
- }
- }
-}
-
-.ai-artifact__panel--wrapper {
- opacity: 0;
- transition: opacity 0.5s ease-in-out;
-}
-
-.ai-artifact__expanded {
- position: fixed;
- top: 0;
- left: 0;
- height: 100%;
- width: 100%;
- z-index: z("fullscreen");
- background-color: var(--secondary);
-
- .ai-artifact__footer {
- display: none;
- }
-
- .ai-artifact__panel--wrapper {
- position: absolute;
- top: 2em;
- left: 2em;
- right: 2em;
- height: 2em;
- z-index: 1000000;
- display: flex;
- justify-content: center;
- opacity: 1;
- }
-
- .ai-artifact__panel {
- display: block;
- position: absolute;
- animation:
- artifact-fade 0.75s forwards,
- artifact-remove 1s forwards;
- animation-delay: 4s;
- background-color: var(--primary);
- opacity: 0.9;
- border-radius: var(--d-button-border-radius);
- transform: translateY(0);
- box-shadow: var(--shadow-card);
- font-size: var(--font-up-1);
-
- &:hover {
- animation-play-state: paused;
- opacity: 1;
- }
-
- button {
- box-sizing: border-box;
- justify-content: center;
- color: var(--secondary-very-high);
- margin: 0 auto;
-
- &:hover {
- color: var(--secondary-very-high);
-
- .d-icon {
- color: var(--secondary-high);
- }
- }
- }
- }
-
- iframe {
- position: fixed;
- top: 0;
- height: 100%;
- max-height: 100%;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: z("fullscreen");
- }
-}
-
-.ai-share-full-topic-modal__body {
- .ai-artifact-controls {
- display: flex;
- justify-content: space-between;
- }
-}
diff --git a/assets/stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss b/assets/stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss
deleted file mode 100644
index 83b9cfed..00000000
--- a/assets/stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss
+++ /dev/null
@@ -1,285 +0,0 @@
-@use "lib/viewport";
-
-@keyframes fade-in {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
-}
-
-.ai-search-discoveries {
- &__regular-results-title {
- margin-top: 0.5em;
- margin-bottom: 0;
- }
-
- &__completion {
- margin: 0;
- }
-
- &__discovery {
- &.preview {
- height: 3.5em; // roughly the loading skeleton height
- overflow: hidden;
- position: relative;
-
- &::after {
- content: "";
- position: absolute;
- display: block;
- background: linear-gradient(rgba(255, 255, 255, 0), var(--secondary));
- height: 50%;
- width: 100%;
- bottom: 0;
- opacity: 0;
- animation: fade-in 0.5s ease-in forwards;
- }
- }
- }
-
- &__discoveries-title,
- &__regular-results-title {
- padding-bottom: 0.5em;
- border-bottom: 1px solid var(--primary-low);
- font-size: var(--font-0);
-
- .d-icon {
- color: var(--primary-high);
- }
- }
-
- &__discoveries-title {
- display: flex;
- justify-content: space-between;
- }
-
- &__toggle {
- padding-left: 0;
- margin-bottom: 0.5em;
- }
-
- .cooked p:first-child {
- margin-top: 0;
- }
-
- &__continue-conversation {
- margin-block: 1rem;
- }
-}
-
-.ai-search-discoveries-tooltip {
- &__content {
- padding: 0.5rem;
- }
-
- &__header {
- font-weight: bold;
- margin-bottom: 0.5em;
- }
-
- &__actions {
- display: flex;
- justify-content: space-between;
- gap: 1rem;
- margin-top: 1rem;
-
- .btn {
- padding: 0;
- }
- }
-
- .fk-d-tooltip__trigger {
- vertical-align: middle;
- }
-
- .d-icon {
- color: var(--primary-medium);
- }
-}
-
-.full-page-discoveries {
- padding: 0 1rem;
-
- @include viewport.until(md) {
- padding: 0.25rem 1rem 0 1rem;
- }
-}
-
-.d-icon-discobot {
- // appears a little undersized next to other icons
- height: 1.15em;
- width: 1.15em;
-}
-
-.ai-discobot-discoveries {
- padding-top: 0.5em;
-}
-
-@include breakpoint("medium", min-width) {
- .search-menu .menu-panel:has(.ai-search-discoveries__discoveries-title) {
- width: 80vw;
- max-width: 900px;
- transition: width 0.5s;
-
- .results {
- display: grid;
- grid-template-columns: 58% 38%;
- grid-template-rows: auto auto 1fr;
- gap: 0 4%;
-
- * {
- // covers all non-discovery content
- grid-column-start: 1;
- }
-
- .ai-discobot-discoveries {
- // always in the second column, always spans all rows
- grid-column-start: 2;
- grid-row: 1 / -1;
- box-sizing: border-box;
- padding: 0 0.5em 0 2em;
- margin: 0.75em 0 0 0;
- border-left: 1px solid var(--primary-low);
-
- .cooked {
- font-size: var(--font-down-1);
- }
- }
- }
-
- .ai-search-discoveries {
- font-size: var(--font-0);
- color: var(--primary-high);
- padding-right: 0.5em;
- }
-
- .ai-search-discoveries__regular-results-title {
- display: none;
- }
-
- .ai-search-discoveries__toggle {
- display: none;
- }
-
- .ai-search-discoveries__discovery.preview {
- height: 100%;
-
- &::after {
- display: none;
- }
- }
- }
-}
-
-.search-page .ai-search-discoveries__discoveries-wrapper {
- padding-bottom: 0.5rem;
-}
-
-.ai-search-discoveries__discoveries-title.full-page-discoveries {
- border: none;
- padding-top: 1rem;
-}
-
-@mixin discoveries-sidebar {
- .full-page-discoveries {
- padding: 1em 10%;
- }
-
- &.search-page.has-discoveries {
- .semantic-search__container {
- background: transparent;
- margin: 0;
- }
-
- .semantic-search__container .semantic-search__results {
- .semantic-search__searching {
- margin-left: 0;
- }
-
- .semantic-search__searching-text {
- margin-left: 1.25em;
- }
- }
-
- .search-container .search-header {
- padding: 1em 2em;
- }
-
- .semantic-search__container .search-results,
- .search-container .search-advanced .search-results,
- .search-container .search-advanced .search-info {
- padding: 1em 2em;
- }
-
- .search-results .fps-result {
- padding: 0;
- margin-bottom: 2.5em;
- }
-
- .search-advanced {
- display: grid;
- grid-template-columns: 70% 30%;
- grid-auto-rows: auto;
-
- > * {
- grid-column: 1;
- align-self: start;
- }
- }
-
- .search-info {
- grid-row: 1;
- }
-
- .ai-search-discoveries__discoveries-title {
- border: none;
- margin-bottom: 0;
- padding-bottom: 0;
- }
-
- .ai-search-discoveries__discoveries-wrapper {
- grid-column: 2 / -1;
- grid-row: 1 / 5;
- border-left: 1px solid var(--primary-low);
- align-self: stretch;
-
- .cooked {
- color: var(--primary-high);
- }
- }
- }
-}
-
-body:not(.has-sidebar-page) {
- @include viewport.from(md) {
- @include discoveries-sidebar;
- }
-}
-
-body.has-sidebar-page {
- @include viewport.from(lg) {
- @include discoveries-sidebar;
- }
-
- @include viewport.between(md, lg) {
- .ai-search-discoveries__discoveries-wrapper {
- padding-bottom: 0;
- }
-
- .ai-search-discoveries__discoveries-title {
- padding-top: 1rem;
- }
-
- .search-container .search-advanced .search-info,
- .semantic-search__container.search-results {
- padding-inline: 10%;
- }
-
- .full-page-discoveries {
- padding-inline: 10%;
- }
- }
-}
diff --git a/assets/stylesheets/modules/ai-bot/common/ai-persona.scss b/assets/stylesheets/modules/ai-bot/common/ai-persona.scss
deleted file mode 100644
index 1b96fc1c..00000000
--- a/assets/stylesheets/modules/ai-bot/common/ai-persona.scss
+++ /dev/null
@@ -1,333 +0,0 @@
-@use "lib/viewport";
-
-.admin-contents .ai-persona-list-editor {
- margin-top: 0;
-}
-
-.ai-persona-list-editor {
- @include viewport.until(md) {
- td {
- border: none;
- padding: 0;
-
- &.d-admin-row__llms,
- &.d-admin-row__features {
- padding-block: 0;
-
- .--card-label {
- display: inline-block;
- font-size: var(--font-down-1);
- color: var(--primary-high);
- }
- }
- }
- }
-
- &__header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin: 0 0 1em 0;
-
- h3 {
- margin: 0;
- }
- }
-
- &__current {
- padding-left: 20px;
- }
-
- li.disabled {
- opacity: 0.5;
- }
-
- &__controls {
- display: flex;
- gap: var(--space-2);
- margin-bottom: var(--space-4);
-
- .filter-input-container {
- flex: 4 1 auto;
- }
-
- .d-select {
- flex: 1 1 auto;
- width: auto;
- height: auto;
- }
- }
-
- &__no-results {
- display: flex;
- flex-direction: column;
- text-align: center;
- justify-content: center;
- padding: var(--space-6);
- gap: var(--space-2);
-
- h3 {
- font-weight: normal;
- }
-
- .btn {
- align-self: center;
- }
- }
-
- &.--layout-table {
- .--card-label {
- display: none;
- }
- }
-
- &.--layout-card {
- tbody {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(18em, 1fr));
- gap: var(--space-4);
- border: none;
- }
-
- thead {
- display: none;
- }
-
- .d-admin-row__content {
- display: grid;
- grid-template-rows: auto 1fr auto auto;
- grid-template-columns: 1fr auto;
- border: 1px solid var(--primary-low);
- padding: var(--space-2) var(--space-4) var(--space-4);
- border-radius: var(--d-border-radius);
-
- .d-admin-row__overview,
- .ai-persona-list__name-with-description {
- display: contents;
- }
-
- .ai-persona-list__name {
- grid-row: 1;
- grid-column: 1;
- font-size: var(--font-up-1);
- display: inline;
- align-self: center;
-
- .avatar {
- height: 1.25em;
- position: relative;
- bottom: 0.15em;
- }
- }
-
- .ai-persona-list__description {
- grid-row: 2;
- grid-column: 1;
- margin: var(--space-2) 0;
- }
-
- .d-admin-row__controls {
- grid-row: 1;
- grid-column: 2;
- }
-
- .d-admin-row__features {
- grid-row: 4;
- grid-column: 1 / span 2;
- padding: 0;
-
- .btn {
- margin-top: var(--space-0);
- }
- }
-
- .d-admin-row__llms {
- grid-row: 3;
- grid-column: 1 / span 2;
- padding: 0;
-
- .btn {
- margin-top: var(--space-2);
- }
- }
-
- .--card-label {
- color: var(--primary-high);
- font-size: var(--font-down-1);
- }
- }
- }
-
- .ai-persona-list {
- &__row-item-feature {
- padding: 0;
- text-align: left;
- }
-
- &__description {
- color: var(--primary-high);
- }
-
- &__name {
- display: flex;
- align-items: center;
- gap: var(--space-1);
- font-size: var(--font-up-0);
- color: var(--primary);
- margin: 0;
- padding: 0;
- line-height: var(--line-height-medium);
-
- .avatar {
- width: auto;
- height: 1.25em;
- }
- }
-
- &__row {
- &:hover {
- background: transparent;
- }
-
- &__overview {
- padding-left: var(--space-2);
- }
- }
- }
-
- .d-admin-row__row-feature-list {
- color: var(--primary-medium);
- }
-}
-
-.ai-persona-tool-option-editor {
- &__instructions {
- color: var(--primary-medium);
- font-size: var(--font-down-1);
- line-height: var(--line-height-large);
- }
-}
-
-.ai-personas__container {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 10px;
- width: 100%;
-}
-
-.ai-persona-editor {
- padding-left: 0.5em;
-
- &__tool-options {
- padding: 1em;
- border: 1px solid var(--primary-low-mid);
- width: 480px;
- }
-
- &__tool-options-name {
- margin-bottom: 10px;
- font-size: var(--font-down-1);
- }
-
- &__response-format {
- width: 100%;
- display: block;
- }
-
- &__response-format-pre {
- margin-bottom: 0;
- white-space: pre-line;
- }
-
- &__response-format-none {
- margin-bottom: 1em;
- margin-top: 0.5em;
- }
-}
-
-.rag-options {
- &__indexing-options {
- display: block;
- margin-top: 1em;
- margin-bottom: 1em;
- }
-}
-
-.rag-uploader {
- &__search-input {
- display: flex;
- align-items: center;
- border: 1px solid var(--primary-400);
- width: 100%;
- box-sizing: border-box;
- height: 35px;
- padding: 0 0.5rem;
-
- &:focus,
- &:focus-within {
- @include default-focus;
- }
-
- &-container {
- display: flex;
- flex-grow: 1;
- }
-
- &__search-icon {
- background: none !important;
- color: var(--primary-medium);
- }
-
- &__input {
- width: 100% !important;
- }
-
- &__input,
- &__input:focus {
- margin: 0 !important;
- border: 0 !important;
- appearance: none !important;
- outline: none !important;
- background: none !important;
- }
- }
-
- &__uploads-list {
- &:has(tr) {
- margin-bottom: 20px;
- }
-
- tbody {
- border-top: none;
- }
- }
-
- &__upload-status {
- text-align: right;
- padding-right: 0;
-
- .indexed {
- color: var(--success);
- }
-
- .uploaded,
- .indexing {
- color: var(--primary-low-mid);
- }
- }
-
- &__remove-file {
- text-align: right;
- padding-left: 0;
- }
-
- &__rag-file-icon {
- margin-right: 5px;
- }
-
- .hidden-upload-field {
- visibility: hidden;
- position: absolute;
- }
-}
diff --git a/assets/stylesheets/modules/ai-bot/common/ai-tools.scss b/assets/stylesheets/modules/ai-bot/common/ai-tools.scss
deleted file mode 100644
index acbe5d8c..00000000
--- a/assets/stylesheets/modules/ai-bot/common/ai-tools.scss
+++ /dev/null
@@ -1,64 +0,0 @@
-@use "lib/viewport";
-
-.ai-tool-parameter {
- padding: 1.5em;
- border: 1px solid var(--primary-low-mid);
- border-radius: var(--d-input-border-radius);
- width: 100%;
- box-sizing: border-box;
-
- .form-kit__container-content {
- flex-direction: column;
- width: 100%;
- }
-}
-
-.ai-tool-parameter__enum-values {
- margin-block: 1rem;
-
- .form-kit__container-content {
- display: grid;
- grid-template-columns: 1fr auto;
- position: relative;
-
- .form-kit__button.btn-icon-text {
- justify-self: start;
- grid-column: 1 / -1;
- }
- }
-}
-
-.ai-tool-editor {
- @include viewport.from(lg) {
- max-width: 80%;
- }
- position: relative;
-
- #control-rag_uploads .rag-uploader {
- h3,
- p {
- display: none;
- }
- }
-}
-
-.ai-tool-test-modal {
- &__test-result div {
- ul {
- padding-left: 1em;
- }
- }
-}
-
-.ai-tool-list-editor {
- &__header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin: 0 0 1em 0;
-
- h3 {
- margin: 0;
- }
- }
-}
diff --git a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss
deleted file mode 100644
index 9579b7bd..00000000
--- a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss
+++ /dev/null
@@ -1,150 +0,0 @@
-nav.post-controls .actions button.cancel-streaming {
- display: none;
-}
-
-.ai-bot-chat {
- #reply-control {
- .user-selector,
- .title-and-category,
- #private-message-users {
- display: none;
- }
- }
-
- .persona-llm-selector {
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- .select-kit.single-select.dropdown-select-box ul.select-kit-collection {
- max-height: 200px;
- }
- margin-bottom: 1em;
- }
-}
-
-.ai-bot-pm {
- .gpt-persona {
- margin-bottom: 5px;
- }
-
- #reply-control .composer-fields {
- .mini-tag-chooser,
- .add-warning {
- display: none;
- }
- }
-}
-
-.ai-bot-chat-warning {
- color: var(--tertiary);
- background-color: var(--tertiary-low);
- border-top: 1px solid var(--tertiary-medium);
- opacity: 0.75;
-
- .d-icon {
- color: var(--tertiary);
- }
- margin: 0;
- padding: 4px 10px;
- width: calc(100% - 20px);
-}
-
-article.streaming nav.post-controls .actions button.cancel-streaming {
- display: inline-block;
-}
-
-.ai-bot-available-bot-options {
- padding: 0.5em;
-
- .ai-bot-available-bot-content {
- color: var(--primary-high);
- display: flex;
- width: 100%;
- min-width: 320px;
- padding: 0.5em;
-
- .d-button-label {
- flex: 1;
- text-align: left;
- }
-
- &:hover {
- background: var(--primary-low);
- }
- }
-}
-
-.topic-body .persona-flair {
- order: 2;
- font-size: var(--font-down-1);
-}
-
-details.ai-quote {
- > summary {
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- span:first-child {
- margin-right: auto;
- }
-
- span:nth-child(2) {
- font-size: var(--font-down-2);
- background: var(--primary-medium);
- padding: 2px 6px 0;
- color: var(--secondary);
- }
- }
-}
-
-.ai-share-modal {
- .d-modal__footer {
- position: relative;
- padding: 10px 20px 25px;
-
- .btn-primary {
- margin-left: auto;
- }
- }
-
- &__just-copied {
- position: absolute;
- font-size: var(--font-down-1);
- right: 20px;
- bottom: 5px;
- color: var(--success);
- }
-}
-
-span.onebox-ai-llm-title {
- font-weight: bold;
-}
-
-.d-modal.ai-debug-modal {
- --modal-max-width: 99%;
-
- ul {
- padding-left: 1em;
- }
-
- li {
- margin-bottom: 0.2em;
- }
-
- li > ul {
- margin-top: 0.2em;
- margin-bottom: 0.2em;
- }
-}
-
-.ai-debug-modal__tokens__count {
- display: block;
-}
-
-.d-modal ul.ai-debug-modal__nav {
- margin: 0 0 1em;
- padding: 0;
- border-bottom: none;
-}
diff --git a/assets/stylesheets/modules/ai-bot/mobile/ai-persona.scss b/assets/stylesheets/modules/ai-bot/mobile/ai-persona.scss
deleted file mode 100644
index 6353f526..00000000
--- a/assets/stylesheets/modules/ai-bot/mobile/ai-persona.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.ai-persona-editor {
- &__system_prompt,
- &__description,
- .select-kit.multi-select {
- width: 100%;
- }
-}
diff --git a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
deleted file mode 100644
index 20ef5909..00000000
--- a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
+++ /dev/null
@@ -1,762 +0,0 @@
-@use "lib/viewport";
-
-.composer-ai-helper-modal {
- .inline-diff {
- font-family: var(--d-font-family--monospace);
- font-variant-ligatures: none;
- }
-
- .text-preview,
- .inline-diff {
- ins {
- background-color: var(--success-low);
- text-decoration: none;
- }
-
- del {
- background-color: var(--danger-low);
- text-decoration: line-through;
- }
-
- mark {
- background-color: var(--highlight-low);
- border-bottom: 2px solid var(--highlight-high);
-
- ins,
- del {
- background: transparent;
- text-decoration: none;
- }
- }
-
- .same-word {
- color: var(--primary);
- }
-
- .ghost {
- color: var(--primary-low-mid);
- }
-
- .preview-area {
- height: 200px;
- }
- }
-
- @keyframes fadeOpacity {
- 0% {
- opacity: 1;
- }
-
- 100% {
- opacity: 0.5;
- }
- }
-
- &__loading {
- animation: fadeOpacity 1.5s infinite alternate;
- }
-
- &__old-value {
- white-space: pre-wrap;
- border-left: 2px solid var(--danger);
- padding-left: 1rem;
- color: var(--danger);
- margin-bottom: 1rem;
- }
-
- &__new-value {
- border-left: 2px solid var(--success);
- padding-left: 1rem;
- color: var(--success);
- }
-
- .d-modal__footer {
- .regenerate {
- margin-left: auto;
- }
- }
-}
-
-.topic-above-suggested-outlet.related-topics {
- margin: 4.5em 0 1em;
-}
-
-.ai-composer-helper-menu {
- max-width: 25rem;
- list-style: none;
-
- ul {
- margin: 0;
- list-style: none;
- }
-}
-
-.ai-custom-prompt {
- display: flex;
- gap: 0.25rem;
- padding: 0.75em 1rem;
-
- &__input[type="text"] {
- border-color: var(--primary-400);
- margin-bottom: 0;
-
- &::placeholder {
- color: var(--primary-medium);
- }
- }
-}
-
-.ai-helper-loading {
- display: flex;
- padding: 0.5rem;
- gap: 1rem;
- justify-content: flex-start;
- align-items: center;
-
- .dot-falling {
- margin-inline: 1rem;
- margin-left: 1.5rem;
- }
-}
-
-.d-editor-input.loading {
- animation: loading-text 1.5s infinite linear;
-}
-
-@keyframes loading-text {
- 0% {
- color: var(--primary);
- }
-
- 50% {
- color: var(--tertiary);
- }
-
- 100% {
- color: var(--primary);
- }
-}
-
-.ai-helper-highlighted-selection {
- background-color: var(--highlight-low-or-medium);
-}
-
-// AI Typing indicator (taken from: https://github.com/nzbin/three-dots)
-.dot-falling {
- position: relative;
- left: -9999px;
- width: 10px;
- height: 10px;
- border-radius: 5px;
- background-color: var(--tertiary);
- color: var(--tertiary);
- box-shadow: 9999px 0 0 0 var(--tertiary);
- animation: dot-falling 1s infinite linear;
- animation-delay: 0.1s;
-}
-
-.dot-falling::before,
-.dot-falling::after {
- content: "";
- display: inline-block;
- position: absolute;
- top: 0;
-}
-
-.dot-falling::before {
- width: 10px;
- height: 10px;
- border-radius: 5px;
- background-color: var(--tertiary);
- color: var(--tertiary);
- animation: dot-falling-before 1s infinite linear;
- animation-delay: 0s;
-}
-
-.dot-falling::after {
- width: 10px;
- height: 10px;
- border-radius: 5px;
- background-color: var(--tertiary);
- color: var(--tertiary);
- animation: dot-falling-after 1s infinite linear;
- animation-delay: 0.2s;
-}
-
-@keyframes dot-falling {
- 0% {
- box-shadow: 9999px -15px 0 0 rgb(152, 128, 255, 0);
- }
-
- 25%,
- 50%,
- 75% {
- box-shadow: 9999px 0 0 0 var(--tertiary);
- }
-
- 100% {
- box-shadow: 9999px 15px 0 0 rgb(152, 128, 255, 0);
- }
-}
-
-@keyframes dot-falling-before {
- 0% {
- box-shadow: 9984px -15px 0 0 rgb(152, 128, 255, 0);
- }
-
- 25%,
- 50%,
- 75% {
- box-shadow: 9984px 0 0 0 var(--tertiary);
- }
-
- 100% {
- box-shadow: 9984px 15px 0 0 rgb(152, 128, 255, 0);
- }
-}
-
-@keyframes dot-falling-after {
- 0% {
- box-shadow: 10014px -15px 0 0 rgb(152, 128, 255, 0);
- }
-
- 25%,
- 50%,
- 75% {
- box-shadow: 10014px 0 0 0 var(--tertiary);
- }
-
- 100% {
- box-shadow: 10014px 15px 0 0 rgb(152, 128, 255, 0);
- }
-}
-
-// Suggest Titles Related
-.showing-ai-suggestions {
- .title-input {
- // border on focus should be on top of suggestion button
- input:focus {
- z-index: 1;
- }
- }
-
- #edit-title {
- padding-right: 2em;
- }
-
- .category-chooser {
- .select-kit-header-wrapper {
- padding-right: 1.5em;
- }
- }
-
- .mini-tag-chooser {
- .multi-select-header {
- padding-right: 2em;
- }
- }
-
- .select-kit.is-expanded {
- // need to raise the z-index so the sibling input buttons don't cover the dropdown
- z-index: z("dropdown") + 1;
-
- + button {
- z-index: z("dropdown") + 1;
- }
- }
-}
-
-.suggestion-button {
- .d-icon-spinner {
- animation: spin 1s linear infinite;
- }
-}
-
-.edit-title__wrapper,
-.edit-category__wrapper,
-.edit-tags__wrapper {
- position: relative;
-}
-
-.suggest-titles-button,
-.suggest-tags-button,
-.suggest-category-button {
- position: absolute;
- right: 0;
- top: 1px; // container border width
- z-index: z("dropdown");
-
- #reply-control & {
- z-index: z("composer", "dropdown") + 1;
- }
-}
-
-#reply-control {
- .composer-actions.is-expanded,
- .composer-popup {
- // need to raise the z-index here
- // because we need another layer to put the AI icon above dropdowns
- // while also keeping them below the composer tips
- z-index: z("composer", "dropdown") + 2;
- }
-
- .with-category .showing-ai-suggestions .category-input {
- flex-wrap: nowrap;
- max-width: calc(50% - 0.2em);
-
- .category-chooser {
- min-width: 0;
- flex: 1 1 auto;
- }
- }
-
- .with-category:not(.with-tags) {
- // when tagging is disabled
- .showing-ai-suggestions .category-input {
- max-width: 40%;
- }
- }
-
- .with-tags {
- .showing-ai-suggestions .tags-input {
- display: flex;
- max-width: calc(50% - 0.2em);
-
- .mini-tag-chooser {
- min-width: 0;
- }
- }
- }
-
- .showing-ai-suggestions {
- #reply-title {
- padding-right: 2em;
- }
- }
-}
-
-.ai-category-suggester-content,
-.ai-tag-suggester-content,
-.ai-title-suggester-content {
- z-index: z("composer", "dropdown");
-}
-
-.ai-suggestions-menu .btn {
- text-align: left;
-}
-
-.mobile-view {
- .ai-category-suggester-content,
- .ai-tag-suggester-content,
- .ai-title-suggester-content {
- z-index: z("modal", "dropdown");
- }
-}
-
-.ai-category-suggester-content {
- .category-row {
- padding: 0.25em 0.5em;
- color: var(--primary-high);
-
- &:hover {
- background: var(--d-hover);
- }
- }
-
- .topic-count {
- font-size: var(--font-down-2);
- }
-}
-
-.ai-tag-suggester-content {
- .tag-row {
- .discourse-tag-count {
- margin-left: 5px;
- }
-
- .d-button-label {
- display: none;
- }
- }
-}
-
-.edit-topic-title {
- .suggestion-button {
- margin: 0;
- padding: 0.465rem;
- }
-}
-
-#topic-title .edit-topic-title.showing-ai-suggestions {
- #edit-title {
- flex: 1 1 90%;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
-
- .suggest-titles-button {
- padding: 0.5rem;
- }
-}
-
-.suggest-tags-button + .ai-suggestions-menu {
- top: 4.25rem;
-}
-
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
-
- 100% {
- transform: rotate(359deg);
- }
-}
-
-.ai-post-helper {
- &__suggestion {
- display: flex;
- flex-direction: column;
-
- h2 {
- font-size: var(--font-0);
- border-bottom: 1px solid var(--primary-low);
- padding-bottom: 0.5em;
- }
-
- p {
- margin: 0;
- }
-
- &__copy {
- .d-icon-check {
- color: var(--success);
- }
- }
-
- &__text {
- padding: 0.5rem;
- }
-
- &__buttons {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-
- .btn {
- flex-grow: 1;
- padding-inline: 0;
- }
- }
- }
-
- &__fast-edit {
- .fast-edit-container {
- padding: 0.75em 1rem;
- }
- }
-}
-
-.choose-topic-modal .split-new-topic-form {
- .control-group {
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- gap: 0.25em;
- margin-bottom: 1rem;
-
- label {
- flex: 100%;
- }
-
- input,
- .combo-box,
- .multi-select {
- flex: 1;
- margin-bottom: 0;
- }
- }
-
- .ai-split-topic-suggestion-button {
- .d-icon-spinner {
- animation: spin 1s linear infinite;
- }
- }
-}
-
-.ai-split-topic-suggestion__results {
- list-style: none;
- margin: 0;
-
- .btn {
- display: block;
- width: 100%;
- text-align: left;
- background: none;
-
- &:hover,
- &:focus {
- background: var(--d-hover);
- color: var(--primary);
- }
- }
-
- li:not(:last-child) {
- border-bottom: 1px solid var(--primary-low);
- }
-
- .ai-split-topic-suggestion__category-result {
- font-size: var(--font-0);
- padding: 0.5em 1rem;
-
- &:hover,
- &:focus {
- background: var(--d-hover);
- cursor: pointer;
- }
- }
-
- .topic-count {
- font-size: var(--font-down-2);
- color: var(--primary-high);
- }
-}
-
-.fk-d-menu[data-identifier="ai-split-topic-suggestion-menu"] {
- z-index: z("modal", "dropdown");
-}
-
-.ai-split-topic-loading-placeholder {
- .d-icon-spinner {
- animation: spin 1s linear infinite;
- }
-
- + .ai-split-topic-suggestion-button {
- display: none;
- }
-}
-
-.thumbnail-suggestions-modal {
- .ai-thumbnail-suggestions {
- display: flex;
- flex-flow: row wrap;
- position: relative;
- gap: 0.5em;
-
- &__item {
- flex: 35%;
- position: relative;
- }
-
- img {
- width: 100%;
- height: auto;
- }
-
- .btn {
- position: absolute;
- top: 0.5rem;
- left: 0.5rem;
- }
- }
-}
-
-// AI Image Caption Feature:
-.image-wrapper {
- .button-wrapper {
- .generate-caption {
- background: var(--tertiary-low);
- color: var(--tertiary);
- box-shadow: var(--shadow-dropdown);
- position: absolute;
- white-space: nowrap;
- top: -2rem;
- left: 0.35rem;
- padding: 0.33em 0.75em;
- transition: all 0.25s ease;
-
- .discourse-no-touch & {
- display: none;
- }
-
- .d-icon {
- margin-right: 0.25rem;
- }
-
- &:active {
- box-shadow: none;
- }
-
- &:hover,
- &:focus {
- background: var(--tertiary-400);
- color: var(--tertiary-hover);
- cursor: pointer;
- }
-
- &.disabled {
- pointer-events: none;
- cursor: not-allowed;
- opacity: 0.7;
- }
- }
- }
-
- .discourse-no-touch & {
- &:hover {
- .button-wrapper .generate-caption {
- display: block;
- }
- }
- }
-}
-
-.ai-caption-popup {
- --ai-caption-popup-min-width: 20rem;
- width: auto;
- right: unset;
- padding: 1em;
- top: unset;
- bottom: 0;
-
- .loading-container {
- min-width: var(--ai-caption-popup-min-width);
- }
-
- textarea {
- box-sizing: border-box;
- width: 100%;
- max-width: 40dvw;
- max-height: calc(100dvh - var(--header-offset) - 10em);
- min-height: 3em;
- height: 7em;
- min-width: var(--ai-caption-popup-min-width);
-
- @include viewport.until(md) {
- width: 100%;
- max-width: unset;
- min-width: unset;
- }
- }
-
- .actions {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-
- .credits {
- font-size: var(--font-down-1);
- margin-left: auto;
- color: var(--tertiary);
-
- .desktop-view & {
- // a little extra space for extra narrow desktop view
- @media screen and (max-width: 675px) {
- span {
- display: none;
- }
- }
- }
- }
- }
-
- .spinner {
- border-color: var(--tertiary-600);
- border-right-color: var(--tertiary);
- }
-}
-
-.ai-image-caption-prompt-dialog {
- .dialog-content {
- max-width: 555px;
- }
-}
-
-.auto-image-caption-loader {
- margin-left: 2rem;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- color: var(--primary-high);
-}
-
-// AI Helper Options List
-.ai-helper-options {
- margin: 0;
- list-style: none;
-
- li {
- display: flex;
- align-items: center;
-
- .shortcut {
- border: none;
- background: none;
- font-size: var(--font-down-1);
- color: var(--primary-low-mid);
- margin-left: auto;
- }
- }
-
- &__button {
- justify-content: left;
- text-align: left;
- background: none;
- width: 100%;
- border-radius: 0;
- margin: 0;
- padding: 0.7rem 1rem;
-
- &:focus,
- &:hover {
- color: var(--primary);
- background: var(--d-hover);
-
- .discourse-no-touch & {
- color: var(--primary);
- background: var(--d-hover);
- }
-
- .d-icon {
- color: var(--primary-high);
-
- .discourse-no-touch & {
- color: var(--primary-high);
- }
- }
- }
- }
-}
-
-.fk-d-menu[data-identifier="ai-composer-helper-menu"],
-.fk-d-menu[data-identifier="ai-title-suggester"] {
- z-index: z("modal", "dialog");
-
- .fullscreen-composer & {
- z-index: z("header") + 1;
- }
-
- .mobile-view & {
- z-index: z("mobile-composer");
- }
-}
-
-.fk-d-toasts:has(.ai-proofread-error-toast) {
- top: unset;
- bottom: calc(var(--composer-height) - 5%);
- right: unset;
- left: 0;
-}
-
-@media screen and (min-width: $reply-area-max-width) {
- .has-sidebar-page {
- .fk-d-toasts:has(.ai-proofread-error-toast) {
- transform: translateX(
- calc(
- (100vw - var(--d-max-width) - var(--d-sidebar-width) / 0.5) / 2 +
- 17em + 1rem
- )
- );
- }
- }
-}
diff --git a/assets/stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss b/assets/stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss
deleted file mode 100644
index f2fe3e1a..00000000
--- a/assets/stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.fk-d-menu {
- .ai-post-helper {
- &__suggestion__text,
- &__suggestion__buttons {
- padding: 0.75em 1rem;
- margin: 0;
- }
- }
-}
diff --git a/assets/stylesheets/modules/ai-helper/mobile/ai-helper.scss b/assets/stylesheets/modules/ai-helper/mobile/ai-helper.scss
deleted file mode 100644
index 2e16f4ae..00000000
--- a/assets/stylesheets/modules/ai-helper/mobile/ai-helper.scss
+++ /dev/null
@@ -1,74 +0,0 @@
-.fk-d-menu-modal {
- &.ai-post-helper-menu-content {
- .ai-post-helper {
- &__suggestion__text,
- &__suggestion__buttons {
- padding: 0.75em 1rem;
- margin: 0;
- }
- }
-
- .d-modal__body {
- display: flex;
- flex-direction: column;
- max-height: 100%;
- }
-
- .ai-helper-options {
- padding: 0;
- }
-
- .ai-custom-prompt {
- padding: 0.75em 1rem;
- margin: 0;
- }
-
- .ai-helper-loading {
- justify-content: center;
- }
- }
-
- .ai-post-helper-menu,
- .ai-composer-helper-menu {
- &__selected-text {
- margin: 0.75em 1rem;
- padding: 0.5em;
- border-radius: var(--d-border-radius);
- border: 1px solid var(--primary-low);
- box-shadow: 0 0 4px rgba(0, 0, 0, 0.125);
- overflow: auto;
- overscroll-behavior: contain;
- }
- }
-}
-
-#topic-title .edit-topic-title.showing-ai-suggestions {
- .category-chooser {
- flex: 1 1 90%;
- }
-
- .ai-category-suggester-trigger {
- padding: 0.425em;
- }
-
- .ai-tag-suggester-trigger {
- padding: 0.45em;
- }
-}
-
-.ios-device #topic-title .edit-topic-title.showing-ai-suggestions button {
- &.ai-title-suggester-trigger {
- padding-top: 0.4em;
- padding-bottom: 0.4em;
- }
-
- &.ai-category-suggester-trigger {
- padding-top: 0.4em;
- padding-bottom: 0.25em;
- }
-
- &.ai-tag-suggester-trigger {
- padding-top: 0.4em;
- padding-bottom: 0.3em;
- }
-}
diff --git a/assets/stylesheets/modules/embeddings/common/ai-embedding-editor.scss b/assets/stylesheets/modules/embeddings/common/ai-embedding-editor.scss
deleted file mode 100644
index cda3c483..00000000
--- a/assets/stylesheets/modules/embeddings/common/ai-embedding-editor.scss
+++ /dev/null
@@ -1,74 +0,0 @@
-.ai-embedding-editor {
- padding-left: 0.5em;
-
- .ai-embedding-editor-input {
- width: 350px;
- }
-
- .ai-embedding-editor-tests {
- &__failure {
- color: var(--danger);
- }
-
- &__success {
- color: var(--success);
- }
- }
-
- &__api-key {
- margin-right: 0.5em;
- }
-
- &__secret-api-key-group {
- display: flex;
- align-items: center;
- }
-
- &__matryoshka_dimensions {
- display: flex;
- align-items: flex-start;
- }
-
- &__distance_functions.select-kit {
- .selected-name {
- .d-icon {
- width: 2em;
- height: 2em;
- position: absolute;
-
- + .name {
- margin-left: 2.25em;
- }
- }
- }
-
- .svg-icon-title {
- width: 2em;
- top: -0.5em;
-
- svg {
- width: 2em;
- height: 2em;
- }
- }
- }
-}
-
-.discourse-ai-embeddings {
- .btn-flat.back-button {
- padding-left: 0;
- }
-
- .fk-d-tooltip__icon {
- margin-left: 0.25em;
- color: var(--primary-medium);
- }
-
- textarea + .fk-d-tooltip__trigger {
- vertical-align: top;
- }
-
- .d-icon-circle-exclamation {
- color: var(--danger);
- }
-}
diff --git a/assets/stylesheets/modules/embeddings/common/semantic-related-topics.scss b/assets/stylesheets/modules/embeddings/common/semantic-related-topics.scss
deleted file mode 100644
index 63fffe77..00000000
--- a/assets/stylesheets/modules/embeddings/common/semantic-related-topics.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-.related-topics {
- margin: 4.5em 0 1em;
-}
-
-.more-topics__container {
- h3 .d-icon {
- margin-right: 0.25em;
- color: var(--primary-high);
- font-size: var(--font-down-1);
- }
-
- .nav-pills .d-icon {
- font-size: var(--font-down-1);
- }
-}
diff --git a/assets/stylesheets/modules/embeddings/common/semantic-search.scss b/assets/stylesheets/modules/embeddings/common/semantic-search.scss
deleted file mode 100644
index e3677291..00000000
--- a/assets/stylesheets/modules/embeddings/common/semantic-search.scss
+++ /dev/null
@@ -1,104 +0,0 @@
-@use "lib/viewport";
-
-.semantic-search__container {
- margin: 1rem 0 0 0;
-
- .has-sidebar-page & {
- @include viewport.until(lg) {
- border-top: 1px solid var(--primary-low);
- }
- }
-
- body:not(.has-sidebar-page) & {
- @include viewport.until(md) {
- border-top: 1px solid var(--primary-low);
- }
- }
-
- .semantic-search__results {
- display: flex;
- flex-direction: column;
- align-items: baseline;
-
- .ai-indicator-wave {
- color: var(--primary-medium);
- }
-
- .semantic-search {
- &__searching {
- display: flex;
- align-items: center;
-
- &.in-progress,
- &.unavailable {
- .semantic-search__searching-text {
- color: var(--primary-medium);
- }
- }
-
- svg {
- font-size: var(--font-down-1);
- color: var(--primary-high);
- }
- }
-
- &__searching-text {
- display: inline-block;
- margin-left: 8px;
- }
-
- &__tooltip {
- margin-left: 4px;
- font-size: var(--font-down-1);
- }
-
- &__entries {
- margin-top: 10px;
- }
- }
- }
-}
-
-.search-results {
- .fps-result {
- padding: 0.5rem;
-
- .ai-result__icon {
- display: none;
- }
- }
-
- .ai-result {
- border-radius: var(--d-border-radius);
-
- .ai-result__icon {
- display: inline;
- margin-right: 0.5rem;
- margin-left: auto;
- font-size: var(--font-up-2);
- color: var(--tertiary);
- }
- }
-}
-
-// Hides other buttons and only shows loader
-// while AI quick search is in progress
-.search-input {
- .ai-quick-search-spinner ~ a.clear-search,
- .ai-quick-search-spinner ~ a.show-advanced-search {
- display: none;
- }
-}
-
-@include viewport.until(md) {
- .search-container .search-advanced .semantic-search__container {
- + .search-info {
- padding-inline: 1rem;
- }
-
- &.search-results {
- margin-bottom: 0;
- padding-inline: 1rem;
- }
- }
-}
diff --git a/assets/stylesheets/modules/llms/common/ai-llm-quotas.scss b/assets/stylesheets/modules/llms/common/ai-llm-quotas.scss
deleted file mode 100644
index 30e1e592..00000000
--- a/assets/stylesheets/modules/llms/common/ai-llm-quotas.scss
+++ /dev/null
@@ -1,71 +0,0 @@
-.ai-llm-quotas {
- margin: 1em 0;
-
- &__table {
- width: 100%;
- border-collapse: collapse;
- margin-bottom: 1em;
- }
-
- &__table-head {
- background-color: var(--primary-very-low);
- }
-
- .duration-selector {
- .select-kit {
- width: 150px;
- }
- }
-
- .duration-selector__custom {
- margin-top: 8px;
- }
-
- &__header {
- text-align: left;
- padding: 0.5em;
- font-weight: bold;
- border-bottom: 2px solid var(--primary-low);
-
- &--actions {
- width: 50px;
- }
- }
-
- &__row {
- border-bottom: 1px solid var(--primary-low);
- }
-
- &__cell {
- vertical-align: middle;
- align-items: center;
-
- &--actions {
- text-align: center;
- }
- }
-
- &__input[type="number"] {
- width: 200px;
- padding: 0.5em;
- margin-bottom: 0;
- }
-
- &__group-select {
- width: 200px;
- }
-
- &__delete-btn {
- padding: 0.3em 0.5em;
- }
-
- &__add-btn {
- padding: 0.3em 0.5em;
- }
-}
-
-.ai-llm-quota-modal {
- .fk-d-tooltip__icon {
- color: var(--primary-medium);
- }
-}
diff --git a/assets/stylesheets/modules/llms/common/ai-llms-editor.scss b/assets/stylesheets/modules/llms/common/ai-llms-editor.scss
deleted file mode 100644
index ac7d8669..00000000
--- a/assets/stylesheets/modules/llms/common/ai-llms-editor.scss
+++ /dev/null
@@ -1,142 +0,0 @@
-.ai-llms-list-editor {
- &__header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin: 0 0 1em 0;
- }
-
- &__configured,
- &__templates {
- margin-top: 2em;
-
- h2 {
- font-size: var(--font-up-1);
- }
- }
-}
-
-.ai-llm-editor {
- padding-left: 0.5em;
-
- .ai-llm-editor-tests {
- &__failure {
- color: var(--danger);
- }
-
- &__success {
- color: var(--success);
- }
- }
-}
-
-[class*="ai-llms-list-editor"] {
- h3 {
- font-weight: normal;
- margin: 0;
- line-height: var(--line-height-medium);
- }
-}
-
-.ai-llms-list-editor__configured {
- .d-toggle-switch {
- justify-content: center;
- }
-}
-
-.ai-tool-list-editor__current,
-.ai-persona-list-editor__current,
-.ai-llms-list-editor__configured {
- .d-admin-table {
- tr:hover {
- background: inherit;
- }
-
- @include breakpoint("tablet", min-width) {
- th,
- td {
- &:first-child {
- padding-left: 0;
- }
-
- &:last-child {
- padding-right: 0;
- }
- }
- }
- }
-}
-
-.ai-llms-list-editor__templates {
- &-list {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
- gap: 1em 2em;
- margin-top: 1em;
- padding-top: 1em;
- border-top: 3px solid var(--primary-low);
- }
-
- &-list-item {
- display: grid;
- grid-template-rows: subgrid;
- grid-row: span 4;
- gap: 0;
- margin-bottom: 2em;
-
- @include breakpoint("mobile-extra-large", min-width) {
- margin-bottom: 3em;
- }
- }
-
- .admin-section-landing-item__description {
- color: var(--primary-high);
- margin: 0.25em 0 0.5em;
- line-height: var(--line-height-large);
- align-self: start;
-
- @include breakpoint("mobile-extra-large", min-width) {
- max-width: 17em;
- }
- }
-
- button {
- justify-self: start;
- }
-
- h4 {
- font-size: var(--font-down-1);
- font-weight: normal;
- color: var(--primary-high);
- margin: 0;
- letter-spacing: 0.1px;
- }
-}
-
-.ai-llm-list-editor__usages {
- list-style: none;
- margin: 0.5em 0 0 0;
- display: flex;
- flex-wrap: wrap;
-
- li {
- font-size: var(--font-down-2);
- border-radius: 0.25em;
- background: var(--primary-very-low);
- border: 1px solid var(--primary-low);
- padding: 1px 3px;
- margin-right: 0.5em;
- margin-bottom: 0.5em;
- }
-}
-
-.ai-llm-list__seeded-model {
- color: var(--primary-high);
- font-size: var(--font-down-1);
-}
-
-@include breakpoint("tablet") {
- .ai-llm-list__description {
- max-width: 80%;
- }
-}
diff --git a/assets/stylesheets/modules/llms/common/spam.scss b/assets/stylesheets/modules/llms/common/spam.scss
deleted file mode 100644
index 4c638007..00000000
--- a/assets/stylesheets/modules/llms/common/spam.scss
+++ /dev/null
@@ -1,107 +0,0 @@
-.ai-spam {
- --chart-scanned-color: var(--success);
- --chart-spam-color: var(--danger);
- padding-top: 15px;
-
- &__settings {
- margin-bottom: 2em;
- }
-
- &__enabled {
- display: flex;
- align-items: center;
- gap: 0.4em;
- margin-bottom: 1em;
-
- .fk-d-tooltip__trigger {
- color: var(--primary-high);
- }
- }
-
- &__settings-title {
- margin-bottom: 1em;
- }
-
- &__toggle,
- &__llm,
- &__persona,
- &__instructions {
- margin-bottom: 1em;
- }
-
- &__toggle-label,
- &__llm-label,
- &__persona-label,
- &__instructions-label {
- display: block;
- margin-bottom: 0.5em;
- font-weight: bold;
- }
-
- &__instructions-input {
- width: 100%;
- min-height: 100px;
- margin-bottom: 0.5em;
- }
-
- &__stats {
- margin-top: 2em;
- }
-
- &__errors {
- .alert {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-
- .btn {
- margin-left: auto;
- }
- }
- }
-}
-
-.spam-test-modal {
- &__body {
- min-width: 500px;
- }
-
- &__test-result {
- margin-top: 1.5em;
- padding-top: 1.5em;
- border-top: 1px solid var(--primary-low);
- }
-
- &__verdict {
- font-size: var(--font-up-2);
- font-weight: bold;
- padding: 0.5em;
- border-radius: 0.25em;
- text-align: center;
- margin: 1em 0;
-
- &.is-spam {
- background: var(--danger-low);
- color: var(--danger);
- }
-
- &.not-spam {
- background: var(--success-low);
- color: var(--success);
- }
- }
-
- &__log {
- margin-top: 1em;
-
- pre {
- max-height: 300px;
- overflow-y: auto;
- background: var(--primary-very-low);
- padding: 1em;
- margin: 0.5em 0;
- font-family: monospace;
- white-space: pre-wrap;
- }
- }
-}
diff --git a/assets/stylesheets/modules/llms/common/usage.scss b/assets/stylesheets/modules/llms/common/usage.scss
deleted file mode 100644
index 228decc4..00000000
--- a/assets/stylesheets/modules/llms/common/usage.scss
+++ /dev/null
@@ -1,154 +0,0 @@
-.ai-usage {
- --chart-response-color: rgba(75, 192, 192, 0.8);
- --chart-request-color: rgba(153, 102, 255, 0.8);
- --chart-cached-color: rgba(153, 102, 255, 0.4);
- padding: 1em;
-
- &__filters-dates {
- display: flex;
- flex-direction: column;
- gap: 1em;
- margin-bottom: 1em;
- }
-
- &__period-buttons {
- display: flex;
- gap: 0.5em;
- align-items: center;
-
- .btn {
- padding: 0.5em 1em;
-
- &.btn-primary {
- background: var(--tertiary);
- color: var(--secondary);
- }
- }
- }
-
- &__custom-date-pickers {
- display: flex;
- gap: 1em;
- align-items: center;
- margin-top: 0.5em;
- }
-
- &__filters {
- margin-bottom: 2em;
- }
-
- &__filters-period {
- display: flex;
- align-items: center;
- gap: 1em;
- }
-
- .d-date-time-input-range {
- display: flex;
- gap: 1em;
- align-items: center;
- }
-
- .d-date-time-input-range .from {
- margin: 0;
- }
-
- &__period-label {
- font-weight: bold;
- }
-
- &__summary {
- margin: 2em 0;
- }
-
- &__summary-title {
- margin-bottom: 1em;
- color: var(--primary);
- font-size: 1.2em;
- }
-
- &__charts {
- margin-top: 2em;
- }
-
- &__chart {
- position: relative;
- }
-
- &__chart-container {
- margin-bottom: 2em;
- }
-
- &__chart-title {
- margin-bottom: 1em;
- }
-
- &__breakdowns {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 2em;
- margin-top: 2em;
-
- @media (max-width: 768px) {
- grid-template-columns: none;
- display: flex;
- flex-direction: column;
- }
- }
-
- &__users {
- grid-column: span 2;
-
- .admin-config-area-card__content {
- display: flex;
-
- .ai-usage__users-table {
- &:first-child {
- margin-right: 2em;
-
- &.-double-width {
- margin-right: 0;
- }
- }
-
- &.-double-width {
- .ai-usage__users-username {
- width: auto;
- }
- }
-
- .ai-usage__users-username {
- width: 50px;
- }
- }
- }
- }
-
- &__features-table,
- &__users-table,
- &__models-table {
- width: 100%;
- border-collapse: collapse;
-
- th {
- text-align: left;
- padding: 0.5em;
- border-bottom: 2px solid var(--primary-low);
- }
- }
-
- &__features-row,
- &__users-row,
- &__models-row {
- &:hover {
- background: var(--primary-low);
- }
- }
-
- &__features-cell,
- &__users-cell,
- &__models-cell {
- padding: 0.5em;
- border-bottom: 1px solid var(--primary-low);
- }
-}
diff --git a/assets/stylesheets/modules/sentiment/common/dashboard.scss b/assets/stylesheets/modules/sentiment/common/dashboard.scss
deleted file mode 100644
index 4fd9d412..00000000
--- a/assets/stylesheets/modules/sentiment/common/dashboard.scss
+++ /dev/null
@@ -1,301 +0,0 @@
-.dashboard.dashboard-sentiment {
- .charts {
- display: grid;
- grid-template-columns: repeat(12, 1fr);
- grid-column-gap: 1em;
- grid-row-gap: 1em;
-
- .admin-report {
- grid-column: span 12;
- }
- }
-}
-
-@mixin report-container-box() {
- border: 1px solid var(--primary-low);
- border-radius: var(--d-border-radius);
- padding: 1rem;
-}
-
-.admin-report.sentiment-analysis .body {
- display: flex;
- flex-flow: row wrap;
- gap: 1rem;
-
- .filters {
- @include report-container-box;
- order: 1;
- width: 100%;
- margin-left: 0;
- flex-flow: row wrap;
- align-items: flex-start;
- justify-content: flex-start;
- gap: 0.5rem;
-
- .control {
- min-width: 200px;
- }
-
- .control:nth-of-type(-n + 4) {
- flex: 1;
- }
-
- // Hides tag selector when showing subcategories selector
- .control:nth-of-type(6):nth-last-of-type(3) {
- display: none;
- }
-
- .control:last-child {
- align-self: flex-end;
- }
-
- .control:has(.export-csv-btn) {
- display: none;
- }
- }
-
- .main {
- flex: 100%;
- display: flex;
- flex-flow: row wrap;
- order: 2;
- align-items: flex-start;
- max-height: 100vh;
- }
-}
-
-.admin-report-sentiment-analysis {
- @include report-container-box;
- flex: 2;
- display: flex;
- gap: 2rem 1rem;
- justify-content: space-evenly;
- align-items: center;
- flex-flow: row wrap;
- padding-inline: 0;
- padding-block: 1.5rem;
-
- .admin-report-doughnut {
- padding: 0.25rem;
- }
-
- &__chart-wrapper {
- height: fit-content;
- position: relative;
- transition:
- transform 0.25s ease,
- box-shadow 0.25s ease;
- border-radius: var(--d-border-radius);
-
- .doughnut-chart-title {
- @include ellipsis;
- margin: 0 auto;
- margin-top: 1rem;
- text-align: center;
- }
-
- &:hover {
- box-shadow: var(--shadow-card);
- transform: translateY(-0.5rem);
- cursor: pointer;
- }
- }
-
- &__selected-chart {
- border: 1px solid var(--primary-low);
- border-radius: var(--d-border-radius);
- padding: 1rem;
-
- .doughnut-chart-title {
- font-size: var(--font-up-2);
- margin: 0 auto;
- text-align: center;
- margin-top: 0.3rem;
- padding-top: 2rem;
- }
- }
-
- &__selected-chart-actions {
- display: flex;
- align-items: center;
- padding-bottom: 0.35rem;
- border-bottom: 1px solid var(--primary-low);
-
- .share {
- margin-left: auto;
-
- .d-icon-check {
- color: var(--success);
- }
- }
- }
-}
-
-:root {
- --d-sentiment-report-positive-rgb: 46, 204, 112;
- --d-sentiment-report-neutral-rgb: 149, 166, 167;
- --d-sentiment-report-negative-rgb: 231, 77, 60;
-}
-
-.admin-report-sentiment-analysis-details {
- @include report-container-box;
- flex: 1 1 300px;
- min-width: 300px;
- margin-left: 1rem;
- display: flex;
- flex-flow: column nowrap;
- overflow-y: auto;
- height: 100%;
- padding-top: 0;
-
- &__filters {
- border-bottom: 1px solid var(--primary-low);
- margin-bottom: 1rem;
-
- @include breakpoint("mobile-extra-large") {
- .d-button-label {
- display: none;
- }
- }
- }
-
- &__scores {
- display: flex;
- flex-flow: column wrap;
- align-items: flex-start;
- justify-content: flex-start;
- gap: 0.25rem;
- list-style: none;
- margin-left: 0;
- background: var(--primary-very-low);
- padding: 1rem;
- border-radius: var(--d-border-radius);
-
- .d-icon-face-smile {
- color: rgb(var(--d-sentiment-report-positive-rgb));
- }
-
- .d-icon-face-meh {
- color: rgb(var(--d-sentiment-report-neutral-rgb));
- }
-
- .d-icon-face-angry {
- color: rgb(var(--d-sentiment-report-negative-rgb));
- }
- }
-
- &__post-score {
- border-radius: var(--d-border-radius);
- background: var(--primary-very-low);
- margin-top: 0.5rem;
- padding: 0.25rem;
- font-size: var(--font-down-1);
- display: inline-block;
-
- &[data-sentiment-score="positive"] {
- color: rgb(var(--d-sentiment-report-positive-rgb));
- background: rgba(var(--d-sentiment-report-positive-rgb), 0.1);
- }
-
- &[data-sentiment-score="neutral"] {
- color: rgb(var(--d-sentiment-report-neutral-rgb));
- background: rgba(var(--d-sentiment-report-neutral-rgb), 0.1);
- }
-
- &[data-sentiment-score="negative"] {
- color: rgb(var(--d-sentiment-report-negative-rgb));
- background: rgba(var(--d-sentiment-report-negative-rgb), 0.1);
- }
- }
-
- &__post-list {
- margin-top: 1rem;
-
- .avatar-wrapper,
- .avatar-link {
- width: calc(48px * 0.75);
- height: calc(48px * 0.75);
- }
-
- img.avatar {
- width: 100%;
- height: 100%;
- }
- }
-}
-
-.admin-reports.admin-contents .sentiment-analysis {
- .horizontal-overflow-nav {
- background: var(--secondary);
- position: sticky;
- top: 0;
- padding-top: 1rem;
- z-index: z("timeline");
- }
-}
-
-.showing-sentiment-analysis-chart
- .admin-report.sentiment-analysis
- .body
- .filters {
- // Hide elements 2 - 6 when showing selected chart
- // as they're not supported being changed in this view
- .control:first-of-type {
- flex: unset;
- }
-
- .control:nth-of-type(n + 2):nth-of-type(-n + 6) {
- display: none;
- }
-}
-
-.sentiment-analysis-table {
- margin: 1rem;
-
- &__total-score {
- font-weight: bold;
- font-size: var(--font-up-1);
- }
-
- &__row {
- cursor: pointer;
- }
-}
-
-.sentiment-horizontal-bar {
- display: flex;
-
- &__count {
- font-weight: bold;
- font-size: var(--font-down-1);
- color: var(--secondary);
- }
-
- &__positive,
- &__neutral,
- &__negative {
- display: flex;
- flex-flow: column nowrap;
- justify-content: flex-end;
- align-items: center;
- padding: 0.75rem;
- border-left: 2px solid var(--secondary);
- border-right: 2px solid var(--secondary);
- }
-
- &__positive {
- background: rgb(var(--d-sentiment-report-positive-rgb));
- border-top-left-radius: var(--d-border-radius);
- border-bottom-left-radius: var(--d-border-radius);
- }
-
- &__negative {
- background: rgb(var(--d-sentiment-report-negative-rgb));
- }
-
- &__neutral {
- background: rgb(var(--d-sentiment-report-neutral-rgb));
- border-top-right-radius: var(--d-border-radius);
- border-bottom-right-radius: var(--d-border-radius);
- }
-}
diff --git a/assets/stylesheets/modules/summarization/common/ai-gists.scss b/assets/stylesheets/modules/summarization/common/ai-gists.scss
deleted file mode 100644
index d7c7a398..00000000
--- a/assets/stylesheets/modules/summarization/common/ai-gists.scss
+++ /dev/null
@@ -1,74 +0,0 @@
-.topic-list-layout-content {
- .btn.--with-description {
- display: grid;
- grid-template-areas: "icon title" "icon description";
- grid-template-columns: auto 1fr;
- text-align: left;
-
- .btn__description {
- grid-area: description;
- width: 100%;
- font-size: var(--font-down-1);
- color: var(--primary-high);
- }
- }
-
- .btn:focus {
- background: transparent;
- }
-
- .btn:focus-visible {
- outline: 2px solid var(--tertiary);
- background: transparent;
- outline-offset: -2px;
- }
-
- .btn.--active {
- background: var(--d-selected);
- }
-}
-
-.topic-list-layout-table-ai {
- .topic-list-item {
- .link-bottom-line {
- font-size: var(--font-down-1);
- margin-top: 0.25em;
- line-height: var(--line-height-medium);
- }
-
- .excerpt {
- width: 100%;
- line-height: var(--line-height-large);
- margin-top: 0.15em;
- margin-bottom: 0.15em;
- color: currentcolor;
-
- &__contents {
- max-width: 70ch;
- overflow-wrap: break-word;
- }
- }
-
- &:not(.visited) {
- .excerpt {
- color: var(--primary-high);
- }
- }
- }
-
- .topic-excerpt {
- display: none;
- }
-
- .mobile-view & {
- .topic-list-item .excerpt {
- margin-top: -0.25em;
- margin-bottom: 0.25em;
- }
-
- .topic-item-stats .num.activity {
- align-self: end;
- margin-bottom: -0.15em; // vertical alignment
- }
- }
-}
diff --git a/assets/stylesheets/modules/summarization/common/ai-summary.scss b/assets/stylesheets/modules/summarization/common/ai-summary.scss
deleted file mode 100644
index 3b8301bf..00000000
--- a/assets/stylesheets/modules/summarization/common/ai-summary.scss
+++ /dev/null
@@ -1,102 +0,0 @@
-.topic-map {
- // Hide the Top Replies label if summarization is enabled
- &:has(.topic-map__additional-contents .ai-summarization-button) {
- .top-replies {
- .d-icon {
- margin: 0;
- height: 1.2em;
- }
-
- .d-button-label {
- display: none;
- }
- }
- }
-
- // Hide the Summarize label when there are many stats
- &:has(.--many-stats):has(.top-replies) .topic-map__additional-contents {
- button {
- .d-icon {
- margin: 0;
- height: 1.2em;
- }
-
- .d-button-label {
- display: none;
- }
- }
- }
-}
-
-.ai-summary-modal {
- .ai-summary {
- &__generating-text {
- display: inline-block;
- margin-left: 3px;
- }
- }
-
- .placeholder-summary {
- padding-top: 0.5em;
- }
-
- .placeholder-summary-text {
- display: inline-block;
- height: 1em;
- margin-top: 0.6em;
- width: 100%;
- }
-
- .generated-summary p {
- margin: 0;
- }
-
- .outdated-summary {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
-
- button {
- margin-top: 0.5em;
- }
-
- p {
- color: var(--primary-medium);
- }
- }
-
- .d-modal__footer {
- display: grid;
- gap: 0;
- grid-template-areas: "summarized regenerate" " outdated regenerate";
- grid-template-columns: 1fr auto;
-
- @include breakpoint(mobile-large) {
- gap: 0.25em 0.5em;
- grid-template-areas: "summarized summarized" "regenerate outdated";
- }
-
- p {
- margin: 0;
- }
-
- .fk-d-tooltip__trigger {
- vertical-align: text-top;
- }
-
- .summary-outdated {
- color: var(--primary-high);
- font-size: var(--font-down-1);
- line-height: var(--line-height-medium);
- }
-
- .summarized-on {
- grid-area: summarized;
- }
-
- button {
- grid-area: regenerate;
- justify-self: start;
- }
- }
-}
diff --git a/assets/stylesheets/modules/summarization/desktop/ai-summary.scss b/assets/stylesheets/modules/summarization/desktop/ai-summary.scss
deleted file mode 100644
index 6d8aed9d..00000000
--- a/assets/stylesheets/modules/summarization/desktop/ai-summary.scss
+++ /dev/null
@@ -1,26 +0,0 @@
-html.scrollable-modal {
- overflow: auto; // overrides core .modal-open class scroll lock
-}
-
-.ai-summary-modal {
- .d-modal__container {
- position: fixed;
- top: var(--header-offset);
- margin-top: 1em;
- right: 1em;
- width: 100vw;
- max-width: 30em;
- max-height: calc(
- 100vh - var(--header-offset) - 3rem - var(--composer-height, 0px)
- );
- box-shadow: var(--shadow-menu-panel);
- }
-
- .fullscreen-composer & {
- display: none;
- }
-}
-
-.ai-summary-modal + .d-modal__backdrop {
- background: transparent; // allows for reading, but still triggers clickoutside event
-}
diff --git a/config/eval-llms.yml b/config/eval-llms.yml
deleted file mode 100644
index 0c43dcc6..00000000
--- a/config/eval-llms.yml
+++ /dev/null
@@ -1,121 +0,0 @@
-llms:
- o3:
- display_name: O3
- name: o3
- tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer
- api_key_env: OPENAI_API_KEY
- provider: open_ai
- url: https://api.openai.com/v1/chat/completions
- max_prompt_tokens: 131072
- vision_enabled: true
- provider_params:
- disable_top_p: true
- disable_temperature: true
-
- gpt-41:
- display_name: GPT-4.1
- name: gpt-4.1
- tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer
- api_key_env: OPENAI_API_KEY
- provider: open_ai
- url: https://api.openai.com/v1/chat/completions
- max_prompt_tokens: 131072
- vision_enabled: true
-
- gpt-4o:
- display_name: GPT-4o
- name: gpt-4o
- tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer
- api_key_env: OPENAI_API_KEY
- provider: open_ai
- url: https://api.openai.com/v1/chat/completions
- max_prompt_tokens: 131072
- vision_enabled: true
-
- gpt-4o-mini:
- display_name: GPT-4o-mini
- name: gpt-4o-mini
- tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer
- api_key_env: OPENAI_API_KEY
- provider: open_ai
- url: https://api.openai.com/v1/chat/completions
- max_prompt_tokens: 131072
- vision_enabled: true
-
- claude-3.5-haiku:
- display_name: Claude 3.5 Haiku
- name: claude-3-5-haiku-latest
- tokenizer: DiscourseAi::Tokenizer::AnthropicTokenizer
- api_key_env: ANTHROPIC_API_KEY
- provider: anthropic
- url: https://api.anthropic.com/v1/messages
- max_prompt_tokens: 200000
- vision_enabled: false
-
- claude-3.5-sonnet:
- display_name: Claude 3.5 Sonnet
- name: claude-3-5-sonnet-latest
- tokenizer: DiscourseAi::Tokenizer::AnthropicTokenizer
- api_key_env: ANTHROPIC_API_KEY
- provider: anthropic
- url: https://api.anthropic.com/v1/messages
- max_prompt_tokens: 200000
- vision_enabled: true
-
- claude-3.7-sonnet:
- display_name: Claude 3.7 Sonnet
- name: claude-3-7-sonnet-latest
- tokenizer: DiscourseAi::Tokenizer::AnthropicTokenizer
- api_key_env: ANTHROPIC_API_KEY
- provider: anthropic
- url: https://api.anthropic.com/v1/messages
- max_prompt_tokens: 200000
- vision_enabled: true
-
- claude-3.7-sonnet-thinking:
- display_name: Claude 3.7 Sonnet
- name: claude-3-7-sonnet-latest
- tokenizer: DiscourseAi::Tokenizer::AnthropicTokenizer
- api_key_env: ANTHROPIC_API_KEY
- provider: anthropic
- url: https://api.anthropic.com/v1/messages
- max_prompt_tokens: 200000
- vision_enabled: true
- provider_params:
- disable_top_p: true
- disable_temperature: true
- enable_reasoning: true
- reasoning_tokens: 1024
-
- gemini-2.0-flash:
- display_name: Gemini 2.0 Flash
- name: gemini-2-0-flash
- tokenizer: DiscourseAi::Tokenizer::GeminiTokenizer
- api_key_env: GEMINI_API_KEY
- provider: google
- url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash
- max_prompt_tokens: 1000000
- vision_enabled: true
-
- gemini-2.5-flash:
- display_name: Gemini 2.5 Flash
- name: gemini-2-5-flash
- tokenizer: DiscourseAi::Tokenizer::GeminiTokenizer
- api_key_env: GEMINI_API_KEY
- provider: google
- url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash
- max_prompt_tokens: 1000000
- vision_enabled: true
- provider_params:
- disable_top_p: true
- disable_temperature: true
-
- gemini-2.0-pro:
- display_name: Gemini 2.0 pro
- name: gemini-2-0-pro
- tokenizer: DiscourseAi::Tokenizer::GeminiTokenizer
- api_key_env: GEMINI_API_KEY
- provider: google
- url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-pro
- max_prompt_tokens: 1000000
- vision_enabled: true
diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml
deleted file mode 100644
index 2b724068..00000000
--- a/config/locales/client.ar.yml
+++ /dev/null
@@ -1,706 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ar:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "يسمح بالبحث باستخدام الذكاء الاصطناعي"
- stream_completion: "يسمح بالتوليد التدريجي للنصوص لشخصيات الذكاء الاصطناعي"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "العاطفة"
- description: "يسرد الجدول عدد المنشورات المصنَّفة بعاطفة محدَّدة. مصنَّفة باستخدام النموذج 'SamLowe/roberta-base-go_emotions'."
- reports:
- filters:
- sort_by:
- label: "الترتيب حسب"
- tag:
- label: "الوسم"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "المُرسِل"
- description: "المستخدم الذي سيرسل التقرير"
- receivers:
- label: "المستلمون"
- description: "المستخدمون الذين سيستلمون التقرير (سيتم إرسال رسائل إلكترونية مباشرة إلى عناوين البريد الإلكتروني، وسيتم إرسال رسائل خاصة إلى أسماء المستخدمين)"
- topic_id:
- label: "معرِّف الموضوع"
- description: "معرِّف الموضوع الذي سيتم نشر التقرير فيه"
- title:
- label: "العنوان"
- description: "عنوان التقرير"
- days:
- label: "الأيام"
- description: "الفترة الزمنية للتقرير"
- offset:
- label: "الإزاحة"
- description: "قد ترغب في تشغيل التقرير تاريخيًا عند الاختبار، استخدم الإزاحة لبدء التقرير في تاريخ سابق"
- instructions:
- label: "التعليمات"
- description: "التعليمات المقدَّمة إلى النموذج اللغوي الكبير"
- sample_size:
- label: "حجم العينة"
- description: "عدد المنشورات لعينة التقرير"
- tokens_per_post:
- label: "الرموز لكل منشور"
- description: "عدد رموز النموذج اللغوي الكبير التي سيتم استخدامها لكل منشور"
- model:
- label: "النموذج"
- description: "النموذج اللغوي الكبير لاستخدامه في إنشاء التقرير"
- categories:
- label: "الفئات"
- description: "تصفية الموضوعات لهذه الفئات فقط"
- tags:
- label: "الوسوم"
- description: "تصفية الموضوعات لهذه الوسوم فقط"
- exclude_tags:
- label: "استبعاد الوسوم"
- description: "استبعاد الموضوعات التي تحتوي على هذه الوسوم"
- exclude_categories:
- label: "استبعاد الفئات"
- description: "استبعاد الموضوعات التي تحتوي على هذه الفئات"
- allow_secure_categories:
- label: "السماح بالفئات الآمنة"
- description: "السماح بإنشاء التقرير للموضوعات الموجودة في الفئات الآمنة"
- suppress_notifications:
- label: "منع الإشعارات"
- description: "منع الإشعارات التي قد يُنشئها التقرير عن طريق التحويل إلى محتوى. سيؤدي هذا إلى إعادة رسم خريطة الإشارات والروابط الداخلية."
- debug_mode:
- label: "وضع تصحيح الأخطاء"
- description: "فعِّل وضع تصحيح الأخطاء لرؤية المدخلات والمخرجات الأولية للنموذج اللغوي الكبير"
- priority_group:
- label: "مجموعة الأولوية"
- description: "رتِّب أولوية المحتوى من هذه المجموعة في التقرير"
- temperature:
- label: "درجة الحرارة"
- top_p:
- label: "Top P"
- llm_tool_triage:
- fields:
- model:
- label: "النموذج"
- llm_triage:
- fields:
- system_prompt:
- label: "رسالة مطالبة النظام"
- description: "رسالة المطالبة التي سيتم استخدامها للفرز، تأكَّد من رده بكلمة واحدة يمكنك استخدامها لبدء الإجراء"
- max_post_tokens:
- label: "الحد الأقصى لعدد رموز المنشور"
- description: "الحد الأقصى لعدد الرموز التي سيتم فحصها باستخدام نظام فرز نموذج اللغة الكبير (LLM)"
- stop_sequences:
- label: "تسلسلات الإيقاف"
- description: "أرشد النموذج إلى إيقاف إنشاء الرموز عند الوصول إلى إحدى هذه القيم"
- search_for_text:
- label: "البحث عن نص"
- description: "إذا ظهر النص التالي في رد نموذج اللغة الكبير (LLM)، فطبِّق هذه الإجراءات"
- category:
- label: "الفئة"
- description: "الفئة المراد تطبيقها على الموضوع"
- tags:
- label: "الوسوم"
- description: "الوسوم المراد تطبيقها على الموضوع"
- canned_reply:
- label: "الرد"
- description: "النص الأولي للرد الجاهز المراد نشره في الموضوع"
- canned_reply_user:
- label: "اسم المستخدم للرد"
- description: "اسم المستخدم للمستخدم الذي سينشر الرد الجاهز"
- hide_topic:
- label: "إخفاء الموضوع"
- description: "جعل الموضوع غير مرئي للعامة إذا تم بدؤه"
- flag_type:
- label: "نوع البلاغ"
- description: "نوع البلاغ الذي سيتم تطبيقه على المنشور (منشور عشوائي أو لمجرد التصعيد للمراجعة)"
- flag_post:
- label: "الإبلاغ عن المنشور"
- description: "يبلغ عن المنشور (سواءً كان عشوائيًا أو للمراجعة)"
- include_personal_messages:
- label: "تضمين الرسائل الشخصية"
- description: "افحص الرسائل الشخصية وافرزها أيضًا"
- model:
- label: "النموذج"
- description: "نموذج اللغة المُستخدَم في الفرز"
- temperature:
- label: "درجة الحرارة"
- discourse_ai:
- title: "الذكاء الاصطناعي"
- features:
- back: "الرجوع"
- disabled: "(متوقفة)"
- groups: "مجموعتان:"
- no_persona: "غير معيَّن"
- no_groups: "لا يوجد"
- edit: "تعديل"
- expand_list:
- zero: "(%{count} أكثر)"
- one: "(%{count} أكثر)"
- two: "(%{count} أكثر)"
- few: "(%{count} أكثر)"
- many: "(%{count} أكثر)"
- other: "(%{count} أكثر)"
- collapse_list: "(عرض أقل)"
- filters:
- all: "الكل"
- reset: "إعادة التعيين"
- search:
- name: "البحث"
- embeddings:
- name: "التضمينات"
- ai_helper:
- name: "المساعد"
- proofread: تدقيق النص لغويًا
- explain: "الشرح"
- smart_dates: "التواريخ الذكية"
- markdown_tables: "إنشاء جدول Markdown"
- custom_prompt: "رسالة مطالبة مخصَّصة"
- spam:
- name: "عشوائي"
- description: "يحدِّد السلوك العشوائي المُحتمَل باستخدام نموذج اللغة الكبير المُحدَّد، ويضع علامة عليه ليقوم مشرفو الموقع بفحصه في قائمة المراجعة"
- modals:
- select_option: "حدِّد خيارًا..."
- spam:
- short_title: "عشوائي"
- title: "تكوين التعامل مع السلوك العشوائي"
- select_llm: "تحديد نموذج اللغة الكبير"
- custom_instructions: "التعليمات المُخصَّصة"
- custom_instructions_help: "تعليمات مُخصَّصة خاصة بموقعك للمساعدة في توجيه الذكاء الاصطناعي في تحديد السلوك العشوائي؛ على سبيل المثال، \"كُن أكثر شراسة في مسح المنشورات غير المكتوبة باللغة العربية\"."
- last_seven_days: "آخر 7 أيام"
- scanned_count: "المنشورات التي تم فحصها"
- false_positives: "تم الإبلاغ بشكلٍ غير صحيح"
- false_negatives: "تم تفويت سلوك عشوائي"
- spam_detected: "تم اكتشاف سلوك عشوائي"
- custom_instructions_placeholder: "تعليمات خاصة بالموقع للذكاء الاصطناعي للمساعدة في تحديد السلوك العشوائي بشكلٍ أكثر دقة"
- enable: "تفعيل"
- spam_tip: "سيفحص نظام اكتشاف السلوك العشوائي بالذكاء الاصطناعي أول 3 منشورات لجميع المستخدمين الجُدد في الموضوعات العامة. وسيقوم بتمييزها للمراجعة وحظر المستخدمين إذا كان من المُرجَّح أن يكون سلوكهم عشوائيًا."
- settings_saved: "تم حفظ الإعدادات"
- spam_description: "يحدِّد السلوك العشوائي المُحتمَل باستخدام نموذج اللغة الكبير المُحدَّد، ويضع علامة عليه ليقوم مشرفو الموقع بفحصه في قائمة المراجعة"
- no_llms: "لا توجد نماذج لغة كبيرة متوفرة"
- test_button: "اختبار..."
- save_button: "حفظ التغييرات"
- test_modal:
- title: "اختبار اكتشاف السلوك العشوائي"
- post_url_label: "عنوان URL أو مُعرِّف المنشور"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 أو مُعرِّف المنشور"
- result: "النتيجة"
- scan_log: "سجل الفحص"
- run: "تشغيل الاختبار"
- spam: "عشوائي"
- not_spam: "ليس سلوكًا عشوائيًا"
- stat_tooltips:
- incorrectly_flagged: "العناصر التي أبلغ روبوت الذكاء الاصطناعي عنها أنها عشوائية، ولم يوافق المشرفون على ذلك"
- missed_spam: "العناصر التي أبلغ المجتمع عنها أنها عشوائية ولم يكتشفها روبوت الذكاء الاصطناعي، ووافق عليها المشرفون"
- errors:
- scan_not_admin:
- message: "تحذير: لن يعمل فحص السلوك العشوائي بشكلٍ صحيح لأن حساب فحص البريد العشوائي ليس مسؤولًا"
- action: "إصلاح"
- resolved: "تم إصلاح الخطأ!"
- usage:
- short_title: "الاستخدام"
- summary: "الملخص"
- total_tokens: "إجمالي الرموز"
- tokens_over_time: "الرموز بمرور الوقت"
- features_breakdown: "الاستخدام لكل ميزة"
- feature: "الميزة"
- usage_count: "عدد مرات الاستخدام"
- model: "النموذج"
- models_breakdown: "الاستخدام لكل نموذج"
- users_breakdown: "الاستخدام لكل مستخدم"
- all_features: "كل الميزات"
- all_models: "كل النماذج"
- username: "اسم المستخدم"
- total_requests: "إجمالي الطلبات"
- request_tokens: "رموز الطلب"
- response_tokens: "رموز الرد"
- net_request_tokens: "صافي رموز الطلب"
- cached_tokens: "الرموز المخزَّنة مؤقتًا"
- cached_request_tokens: "رموز الطلب المُخزَّنة مؤقتًا"
- no_users: "لم يتم العثور على بيانات استخدام المستخدم"
- no_models: "لم يتم العثور على بيانات استخدام نموذجية"
- no_features: "لم يتم العثور على بيانات استخدام الميزات"
- subheader_description: "الرموز هي الوحدات الأساسية التي تستخدمها نماذج اللغة الكبيرة لفهم النصوص وتوليدها، وقد تؤثر بيانات الاستخدام على التكاليف"
- stat_tooltips:
- total_requests: "جميع الطلبات المُقدَّمة إلى نماذج اللغة الكبيرة من خلال Discourse"
- total_tokens: "جميع الرموز المُستخدَمة عند إدخال رسالة مطالبة في نموذج لغة كبير"
- request_tokens: "الرموز المستخدمة عندما يحاول نموذج اللغة الكبير فهم ما تقوله"
- response_tokens: "الرموز المستخدمة عندما يستجيب نموذج اللغة الكبير لرسالة المطالبة الخاصة بك"
- cached_tokens: "رموز الطلب التي تمت معالجتها مسبقًا والتي يعيد نموذج اللغة الكبير استخدامها لتحسين الأداء والتكلفة"
- periods:
- last_day: "آخر 24 ساعة"
- last_week: "الأسبوع الماضي"
- last_month: "الشهر الماضي"
- custom: "مُخصَّص..."
- ai_persona:
- ai_tools: "الأدوات"
- tool_strategies:
- all: "التطبيق على كل الردود"
- replies:
- zero: "التطبيق على أول %{count} رد فقط"
- one: "التطبيق على الرد الأول فقط"
- two: "التطبيق على أول ردَّين (%{count}) فقط"
- few: "التطبيق على أول %{count} ردود فقط"
- many: "التطبيق على أول %{count} ردًا فقط"
- other: "التطبيق على أول %{count} رد فقط"
- back: "رجوع"
- name: "الاسم"
- edit: "تعديل"
- export: "تصدير"
- description: "الوصف"
- no_llm_selected: "لم يتم تحديد نموذج لغة"
- max_context_posts: "الحد الأقصى للمنشورات السياقية"
- max_context_posts_help: "الحد الأقصى لعدد المنشورات التي سيتم استخدامها كسياق للذكاء الاصطناعي عند الرد على مستخدم. (اترك القيمة فارغة للإعداد الافتراضي)"
- vision_enabled: دعم الرؤية
- vision_enabled_help: إذا تم تفعيله، فسيحاول الذكاء الاصطناعي فهم الصور التي ينشرها المستخدمون في الموضوع، بناءً على النموذج المُستخدَم لدعم الرؤية. هذه الميزة مدعومة بأحدث النماذج من Anthropic وGoogle وOpenAI.
- vision_max_pixels: الحجم المدعوم للصور
- vision_max_pixel_sizes:
- low: جودة منخفضة - الأرخص (256 × 256)
- medium: جودة متوسطة (512 × 512)
- high: جودة عالية - الأبطأ (1024 × 1024)
- tool_details: عرض تفاصيل الأدوات
- tool_details_help: ستعرض تفاصيل المستخدمين النهائيين بشأن الأدوات التي قام نموذج اللغة بتشغيلها.
- mentionable: السماح بالإشارات
- mentionable_help: إذا تم تفعيله، يمكن للمستخدمين في المجموعات المسموح بها الإشارة إلى هذا المستخدم في المنشورات، وسيرد الذكاء الاصطناعي باعتباره هذا الشخص.
- user: المستخدم
- create_user: إنشاء مستخدم
- create_user_help: يمكنك اختياريًا إرفاق مستخدم بهذه الشخصية. إذا قمت بذلك، فسيستخدم الذكاء الاصطناعي هذا المستخدم للرد على الطلبات.
- default_llm: نموذج اللغة الافتراضي
- default_llm_help: نموذج اللغة الافتراضي الذي سيتم استخدامه لهذه الشخصية. مطلوب إذا كنت ترغب في الإشارة إلى شخصية في المنشورات العامة.
- question_consolidator_llm: نموذج اللغة لتوحيد الأسئلة
- question_consolidator_llm_help: نموذج اللغة الذي سيتم استخدامه لتوحيد الأسئلة، يمكنك اختيار نموذج أقل قوة للتوفير في التكاليف.
- system_prompt: رسالة مطالبة النظام
- forced_tool_strategy: استراتيجية الأداة الإجبارية
- allow_chat_direct_messages: "السماح بالرسائل المباشرة في الدردشة"
- allow_chat_direct_messages_help: "إذا تم التفعيل، يمكن للمستخدمين في المجموعات المسموح بها إرسال رسالة مباشرة إلى هذه الشخصية."
- allow_chat_channel_mentions: "السماح بالإشارات في قناة الدردشة"
- allow_chat_channel_mentions_help: "إذا تم التفعيل، يمكن للمستخدمين في المجموعات المسموح بها الإشارة إلى هذه الشخصية في قنوات الدردشة."
- allow_personal_messages: "السماح بالرسائل الشخصية"
- allow_personal_messages_help: "إذا تم التفعيل، يمكن للمستخدمين في المجموعات المسموح بها إرسال رسائل شخصية إلى هذه الشخصية."
- allow_topic_mentions: "السماح بالإشارات في الموضوعات"
- allow_topic_mentions_help: "إذا تم التفعيل، يمكن للمستخدمين في المجموعات المسموح بها الإشارة إلى هذه الشخصية في الموضوعات."
- force_default_llm: "استخدام نموذج اللغة الافتراضي دائمًا"
- save: "حفظ"
- saved: "تم حفظ الشخصية"
- enabled: "مفعَّلة؟"
- tools: "الأدوات المفعَّلة"
- forced_tools: "الأدوات الإجبارية"
- allowed_groups: "المجموعات المسموح بها"
- confirm_delete: "هل تريد بالتأكيد حذف هذه الشخصية؟"
- new: "شخصية جديدة"
- no_personas: "لم تُنشئ أي شخصيات بعد"
- title: "الشخصيات"
- short_title: "الشخصيات"
- delete: "حذف"
- temperature: "درجة الحرارة"
- temperature_help: "درجة الحرارة التي سيتم استخدامها في نموذج اللغة الكبير (LLM)، يزداد الإبداع بزيادة القيمة (اترك القيمة فارغة لاستخدام النموذج الافتراضي، عادةً ما تكون قيمة بين 0.0 و2.0)"
- top_p: "Top P"
- top_p_help: "Top P التي سيتم استخدامها في نموذج اللغة الكبير (LLM)، تزداد العشوائية بزيادة القيمة (اترك القيمة فارغة لاستخدام النموذج الافتراضي، عادةً ما تكون قيمة بين 0.0 و1.0)"
- priority: "الأولوية"
- priority_help: "يتم عرض الشخصيات ذات الأولوية للمستخدمين في أعلى قائمة الشخصيات. إذا كانت الأولوية لعدة أشخاص، فسيتم فرزهم أبجديًا."
- tool_options: "خيارات الأداة"
- rag_conversation_chunks: "البحث في أجزاء المحادثة"
- rag_conversation_chunks_help: "عدد الأجزاء التي سيتم استخدامها لإي عمليات البحث ضمن نموذج RAG. يزداد مقدار السياق الذي يمكن للذكاء الاصطناعي استخدامه بزيادة القيمة."
- persona_description: "تُعد الشخصيات ميزة قوية تتيح لك تخصيص سلوك محرك الذكاء الاصطناعي في منتدى Discourse الخاص بك. إنها تعمل بمثابة \"رسالة نظام\" توجِّه ردود الذكاء الاصطناعي وتفاعلاته، مما يساعد في إنشاء تجربة مستخدم أكثر تخصيصًا وتفاعليةً."
- response_format:
- open_modal: "تعديل"
- modal:
- key_title: "المفتاح"
- filters:
- reset: "إعادة التعيين"
- rag:
- options:
- rag_chunk_tokens: "تحميل أجزاء الرموز"
- rag_chunk_tokens_help: "عدد الرموز المميزة التي سيتم استخدامها لكل جزء في نموذج RAG. يزداد مقدار السياق الذي يمكن للذكاء الاصطناعي استخدامه بزيادة القيمة. (سيؤدي التغيير إلى إعادة فهرسة جميع التحميلات)"
- rag_chunk_overlap_tokens: "تحميل رموز تداخل الأجزاء"
- rag_chunk_overlap_tokens_help: "عدد الرموز المميزة التي ستتداخل بين الأجزاء في نموذج RAG. (سيؤدي التغيير إلى إعادة فهرسة جميع التحميلات)"
- show_indexing_options: "إظهار خيارات التحميل"
- hide_indexing_options: "إخفاء خيارات التحميل"
- uploads:
- title: "التحميلات"
- button: "إضافة الملفات"
- filter: "تصفية التحميلات"
- indexed: "تمت الفهرسة"
- indexing: "جارٍ الفهرسة"
- uploaded: "جاهزة للفهرسة"
- uploading: "جارٍ التحميل..."
- remove: "إزالة التحميل"
- tools:
- back: "رجوع"
- short_title: "الأدوات"
- export: "تصدير"
- no_tools: "لم تُنشئ أي أدوات بعد"
- name: "الاسم"
- new: "أداة جديدة"
- description: "الوصف"
- description_help: "وصف واضح لغرض الأداة بالنسبة إلى نموذج اللغة"
- subheader_description: "تعمل الأدوات على توسيع قدرات روبوتات الذكاء الاصطناعي باستخدام دوال JavaScript يحدِّدها المستخدم."
- summary: "الملخص"
- summary_help: "ملخص الغرض من الأدوات لعرضه للمستخدمين النهائيين"
- script: "البرنامج النصي"
- parameters: "المعلمات"
- save: "حفظ"
- remove_parameter: "إزالة"
- parameter_required: "مطلوب"
- parameter_enum: "Enum"
- parameter_name: "اسم المعلمة"
- parameter_description: "وصف المعلمة"
- enum_value: "قيمة Enum"
- add_enum_value: "إضافة قيمة enum"
- edit: "تعديل"
- test: "تشغيل الاختبار"
- delete: "حذف"
- saved: "تم حفظ الأداة"
- confirm_delete: "هل تريد بالتأكيد حذف هذه الأداة؟"
- test_modal:
- title: "اختبار أداة الذكاء الاصطناعي"
- run: "تشغيل الاختبار"
- result: "نتيجة الاختبار"
- llms:
- short_title: "نماذج اللغة الكبيرة"
- no_llms: "لا توجد نماذج لغة كبيرة بعد"
- new: "نموذج جديد"
- display_name: "الاسم"
- name: "معرِّف النموذج"
- provider: "مقدِّم الخدمة"
- tokenizer: "أداة الترميز"
- url: "عنوان URL لخدمة استضافة النموذج"
- api_key: "مفتاح API لخدمة استضافة النموذج"
- enabled_chat_bot: "السماح بمحدِّد روبوت الذكاء الاصطناعي"
- vision_enabled: "دعم الرؤية"
- ai_bot_user: "مستخدم روبوت الذكاء الاصطناعي"
- save: "حفظ"
- edit: "تعديل"
- saved: "تم حفظ نموذج اللغة الكبير (LLM)"
- back: "رجوع"
- confirm_delete: هل تريد بالتأكيد حذف هذا النموذج؟
- delete: حذف
- seeded_warning: "تم تكوين هذا النموذج مسبقًا على موقعك ولا يمكن تعديله."
- quotas:
- title: "حصص الاستخدام"
- add_title: "إنشاء حصة جديدة"
- group: "المجموعة"
- max_tokens: "الحد الأقصى للرموز"
- max_usages: "الحد الأقصى لمرات الاستخدام"
- duration: "المدة"
- confirm_delete: "هل تريد بالتأكيد حذف هذه الحصة؟"
- add: "إضافة حصة"
- durations:
- hour: "ساعة واحدة"
- six_hours: "6 ساعات"
- day: "24 ساعة"
- week: "7 أيام"
- custom: "مُخصَّص..."
- hours: "الساعات"
- max_tokens_help: "الحد الأقصى لعدد الرموز (الكلمات والأحرف) التي يمكن لكل مستخدم في هذه المجموعة استخدامها خلال المدة المُحدَّدة. الرموز هي الوحدات التي تستخدمها نماذج الذكاء الاصطناعي لمعالجة النص - الرمز الواحد يساوي تقريبًا 4 أحرف أو 3/4 كلمة."
- max_usages_help: "الحد الأقصى لعدد المرات التي يمكن لكل مستخدم في هذه المجموعة استخدام نموذج الذكاء الاصطناعي فيها خلال المدة المُحدَّدة. يتم تتبع هذه الحصة لكل مستخدم على حدة، ولا تتم مشاركتها عبر المجموعة."
- usage:
- ai_bot: "روبوت الذكاء الاصطناعي"
- ai_helper: "المساعد"
- ai_persona: "الشخصية (%{persona})"
- ai_summarization: "تلخيص"
- ai_embeddings_semantic_search: "البحث باستخدام الذكاء الاصطناعي"
- ai_spam: "عشوائي"
- in_use_warning:
- zero: "هذا النموذج مُستخدَم حاليًا من قِبل %{settings}. إذا تم تكوينه بشكلٍ خاطئ، فلن تعمل الميزة كما هو متوقَّع. "
- one: "هذا النموذج مُستخدَم حاليًا من قِبل %{settings}. إذا تم تكوينه بشكلٍ خاطئ، فلن تعمل الميزة كما هو متوقَّع."
- two: "هذا النموذج مُستخدَم حاليًا من قِبل %{settings}. إذا تم تكوينه بشكلٍ خاطئ، فلن تعمل الميزتان كما هو متوقَّع. "
- few: "هذا النموذج مُستخدَم حاليًا من قِبل %{settings}. إذا تم تكوينه بشكلٍ خاطئ، فلن تعمل الميزات كما هو متوقَّع. "
- many: "هذا النموذج مُستخدَم حاليًا من قِبل %{settings}. إذا تم تكوينه بشكلٍ خاطئ، فلن تعمل الميزات كما هو متوقَّع. "
- other: "هذا النموذج مُستخدَم حاليًا من قِبل %{settings}. إذا تم تكوينه بشكلٍ خاطئ، فلن تعمل الميزات كما هو متوقَّع. "
- model_description:
- none: "الإعدادات العامة التي تعمل مع معظم نماذج اللغة"
- anthropic-claude-opus-4-0: "النموذج الأكثر ذكاءً لدى Anthropic"
- anthropic-claude-3-5-haiku-latest: "سريع وفعَّال من حيث التكلفة"
- google-gemini-2-5-flash: "خفيف وسريع وفعَّال من حيث التكلفة مع الاستدلال متعدد النماذج"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "نموذج متعدد اللغات خفيف وفعَّال"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "نموذج قوي متعدد الأغراض"
- mistral-mistral-large-latest: "النموذج الأقوى لدى Mistral"
- mistral-pixtral-large-latest: "النموذج القادر على الرؤية الأقوى لدى Mistral"
- preseeded_model_description: "نموذج مفتوح المصدر تم تكوينه مسبقًا باستخدام %{model}"
- configured:
- title: "تم تكوين نماذج اللغة الكبيرة (LLM)"
- preconfigured_llms: "حدِّد نموذج اللغة الكبير الخاص بك"
- preconfigured:
- title_no_llms: "حدِّد قالبًا للبدء"
- title: "لم يتم تكوين قوالب نماذج اللغة الكبيرة (LLM)"
- description: "نماذج اللغة الكبيرة (LLM) عبارة عن أدوات ذكاء اصطناعي تم تحسينها لتنفيذ مهام مثل تلخيص المحتوى وإنشاء التقارير وأتمتة تفاعلات العملاء وتسهيل إدارة المنتديات وتقديم الرؤى"
- fake: "التكوين اليدوي"
- button: "إعداد"
- next:
- title: "التالي"
- tests:
- title: "تشغيل الاختبار"
- running: "جارٍ تشغيل الاختبار..."
- success: "تم بنجاح!"
- failure: "أرجعت محاولة التواصل مع النموذج هذا الخطأ: %{error}"
- hints:
- name: "إننا نقوم بتضمين هذا في استدعاء واجهة برمجة التطبيقات (API) لتحديد النموذج الذي سنستخدمه"
- vision_enabled: "إذا تم تفعيله، فسيحاول الذكاء الاصطناعي فهم الصور. إنه يعتمد على النموذج المُستخدَم لدعم الرؤية. هذه الميزة مدعومة بأحدث النماذج من Anthropic وGoogle وOpenAI."
- enabled_chat_bot: "إذا تم تفعيله، يمكن للمستخدمين تحديد هذا النموذج عند إنشاء الرسائل الخاصة باستخدام روبوت الذكاء الاصطناعي"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "مخصَّصة"
- provider_fields:
- access_key_id: "معرِّف مفتاح الوصول إلى AWS Bedrock"
- region: "منطقة AWS Bedrock"
- organization: "معرِّف مؤسسة OpenAI الاختياري"
- disable_system_prompt: "إيقاف رسالة النظام في رسائل المطالبة"
- enable_native_tool: "تفعيل دعم الأدوات الأصلية"
- disable_native_tools: "إيقاف دعم الأدوات الأصلية (استخدام أدوات تعتمد على XML)"
- provider_order: "ترتيب مقدِّمي الخدمة (قائمة مفصولة بفاصلة)"
- provider_quantizations: "ترتيب كميات مقدِّمي الخدمة (قائمة مفصولة بفاصلة. على سبيل المثال: fp16,fp8)"
- disable_streaming: "إيقاف التوليد التدريجي للنصوص (تحويل التوليد التدريجي للنصوص إلى التوليد الكامل للنصوص)"
- related_topics:
- title: "الموضوعات ذات الصلة"
- pill: "موضوعات ذات صلة"
- ai_helper:
- title: "اقتراح تغييرات باستخدام الذكاء الاصطناعي"
- description: "اختر أحد الخيارات أدناه، وسيقترح عليك الذكاء الاصطناعي نسخة جديدة من النص."
- selection_hint: "تلميح: يمكنك أيضًا تحديد جزء من النص قبل فتح أداة المساعدة لإعادة كتابة ذلك الجزء فقط."
- suggest: "الاقتراح باستخدام الذكاء الاصطناعي"
- suggest_errors:
- too_many_tags:
- zero: "يمكن أن يكون لديك %{count} وسم فقط كحد أقصى"
- one: "يمكن أن يكون لديك وسم واحد (%{count}) فقط كحد أقصى"
- two: "يمكن أن يكون لديك وسمان (%{count}) فقط كحد أقصى"
- few: "يمكن أن يكون لديك %{count} وسوم فقط كحد أقصى"
- many: "يمكن أن يكون لديك %{count} وسمًا فقط كحد أقصى"
- other: "يمكن أن يكون لديك %{count} وسم فقط كحد أقصى"
- no_suggestions: "لا توجد اقتراحات متوفرة"
- missing_content: "يُرجى إدخال بعض المحتوى لإنشاء الاقتراحات."
- context_menu:
- trigger: "اسأل الذكاء الاصطناعي"
- loading: "الذكاء الاصطناعي قيد إنشاء المحتوى"
- cancel: "إلغاء"
- confirm: "تأكيد"
- discard: "تجاهل"
- changes: "التعديلات المقترحة"
- custom_prompt:
- title: "رسالة مطالبة مخصَّصة"
- placeholder: "أدخل رسالة مطالبة مخصَّصة..."
- submit: "إرسال رسالة مطالبة"
- translate_prompt: "الترجمة إلى %{language}"
- post_options_menu:
- trigger: "اسأل الذكاء الاصطناعي"
- title: "اسأل الذكاء الاصطناعي"
- loading: "الذكاء الاصطناعي قيد إنشاء المحتوى"
- close: "إغلاق"
- copy: "نسخ"
- copied: "تم النسخ!"
- cancel: "إلغاء"
- insert_footnote: "إضافة حاشية سفلية"
- footnote_disabled: "تم إيقاف الإدراج التلقائي، انقر على زر النسخ وقم بتعديله يدويًا"
- footnote_credits: "شرح بواسطة الذكاء الاصطناعي"
- fast_edit:
- suggest_button: "اقتراح تعديل"
- thumbnail_suggestions:
- title: "الصور المصغَّرة المقترحة"
- select: "تحديد"
- selected: "محدَّدة"
- image_caption:
- button_label: "التسمية التوضيحية بالذكاء الاصطناعي"
- generating: "جارٍ إنشاء تسمية توضيحية.."
- credits: "تم إنشاء التسمية التوضيحية بالذكاء الاصطناعي"
- save_caption: "حفظ"
- automatic_caption_setting: "تفعيل التسمية التوضيحية التلقائية"
- automatic_caption_loading: "جارٍ إنشاء تسميات توضيحية للصور..."
- automatic_caption_dialog:
- prompt: "بحتوي هذا المنشور على صور دون تسميات توضيحية. هل ترغب في تمكين التسميات التوضيحية التلقائية عند تحميل الصور؟ (يمكن تغيير هذا في تفضيلاتك لاحقًا)"
- confirm: "تفعيل"
- cancel: "لا تسألني مرة أخرى"
- no_content_error: "ينبغي إضافة المحتوى أولًا لتنفيذ إجراءات الذكاء الاصطناعي عليه"
- reviewables:
- model_used: "النموذج المستخدم:"
- accuracy: "الدقة:"
- embeddings:
- short_title: "التضمينات"
- new: "تضمين جديد"
- back: "رجوع"
- save: "حفظ"
- saved: "تم حفظ تكوين التضمين"
- delete: "حذف"
- confirm_delete: هل تريد بالتأكيد إزالة تكوين التضمين هذا؟
- empty: "لم تقم بإعداد التضمينات بعد"
- presets: "حدِّد إعدادًا مسبقًا..."
- configure_manually: "التكوين يدويًا"
- edit: "تعديل"
- seeded_warning: "تم تكوين هذا مسبقًا على موقعك ولا يمكن تعديله."
- tests:
- title: "تشغيل الاختبار"
- running: "جارٍ تشغيل الاختبار..."
- success: "تم بنجاح!"
- failure: "أدَّت محاولة إنشاء تضمين إلى: %{error}"
- hints:
- dimensions_warning: "لا يمكن تغييرها بعد حفظها."
- matryoshka_dimensions: "تحدِّد حجم التضمينات المتداخلة المُستخدَمة في التمثيل التسلسلي أو متعدد الطبقات للبيانات، وذلك على غرار طريقة ملاءمة الدمى المتداخلة (الماتريوشكا) مع بعضها البعض."
- sequence_length: "الحد الأقصى لعدد الرموز التي يمكن معالجتها في الوقت نفسه عند إنشاء التضمينات أو معالجة استعلام."
- distance_function: "تحدِّد كيفية حساب التشابه بين التضمينات، باستخدام إما مسافة جيب التمام (قياس الزاوية بين المتجهات) أو حاصل الضرب الداخلي السلبي (قياس تداخل قيم المتجهات)."
- display_name: "الاسم"
- provider: "مقدِّم الخدمة"
- url: "عنوان URL لخدمة التضمينات"
- api_key: "مفتاح API لخدمة التضمينات"
- tokenizer: "أداة الترميز"
- dimensions: "أبعاد التضمين"
- max_sequence_length: "طول التسلسل"
- embed_prompt: "أمر التضمين"
- search_prompt: "أمر البحث"
- matryoshka_dimensions: "الأبعاد المتداخلة"
- distance_function: "دالة المسافة"
- distance_functions:
- "<#>": "حاصل الضرب الداخلي السلبي"
- <=>: "مسافة جيب التمام"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "مخصَّصة"
- provider_fields:
- model_name: "اسم النموذج"
- semantic_search: "الموضوعات (دلالية)"
- semantic_search_loading: "جارٍ البحث عن المزيد من النتائج باستخدام الذكاء الاصطناعي"
- semantic_search_results:
- toggle: "جارٍ عرض %{count} من النتائج التي تم العثور عليها باستخدام الذكاء الاصطناعي"
- toggle_hidden: "جارٍ إخفاء %{count} من النتائج التي تم العثور عليها باستخدام الذكاء الاصطناعي"
- none: "عذرًا، لم يعثر بحث الذكاء الاصطناعي لدينا على أي موضوعات مطابقة"
- new: "اضغط على \"بحث\" لبدء البحث عن نتائج جديدة باستخدام الذكاء الاصطناعي"
- unavailable: "نتائج الذكاء الاصطناعي غير متوفرة"
- semantic_search_tooltips:
- results_explanation: "عند التفعيل، سيتم إضافة نتائج بحث الذكاء الاصطناعي الإضافية أدناه."
- invalid_sort: "يجب فرز نتائج البحث حسب الصلة لعرض نتائج الذكاء الاصطناعي"
- semantic_search_unavailable_tooltip: "يجب فرز نتائج البحث حسب الصلة لعرض نتائج الذكاء الاصطناعي"
- ai_generated_result: "تم العثور على نتيجة بحث باستخدام الذكاء الاصطناعي"
- quick_search:
- suffix: "في كل الموضوعات والمنشورات بالذكاء الاصطناعي"
- ai_artifact:
- expand_view_label: "توسيع العرض"
- collapse_view_label: "الخروج من وضع ملء الشاشة (ESC أو زر الرجوع)"
- click_to_run_label: "تشغيل المنتج الثانوي"
- ai_bot:
- llm: "النموذج"
- pm_warning: "تتم مراقبة رسائل روبوت دردشة الذكاء الاصطناعي بانتظام من قِبل المشرفين."
- cancel_streaming: "إيقاف الرد"
- default_pm_prefix: "[رسالة خاصة دون عنوان من روبوت ذكاء اصطناعي]"
- shortcut_title: "بدء رسالة خاصة باستخدام روبوت ذكاء اصطناعي"
- share: "نسخ محادثة الذكاء الاصطناعي"
- conversation_shared: "تم نسخ المحادثة"
- debug_ai: "عرض طلب الذكاء الاصطناعي الأولي والرد"
- debug_ai_modal:
- title: "عرض تفاعل الذكاء الاصطناعي"
- copy_request: "طلب النسخة"
- copy_response: "رد النسخة"
- request_tokens: "رموز الطلب:"
- response_tokens: "رموز الرد:"
- request: "طلب"
- response: "الرد"
- next_log: "التالي"
- previous_log: "السابق"
- share_full_topic_modal:
- title: "مشاركة المحادثة بشكلٍ علني"
- share: "مشاركة ونسخ الرابط"
- update: "تحديث ونسخ الرابط"
- delete: "حذف المشاركة"
- share_ai_conversation:
- name: "مشاركة محادثة الذكاء الاصطناعي"
- title: "مشاركة هذه المحادثة بشكلٍ علني"
- invite_ai_conversation:
- button: "دعوة"
- ai_label: "الذكاء الاصطناعي"
- ai_title: "المحادثة مع الذكاء الاصطناعي"
- share_modal:
- title: "نسخ محادثة الذكاء الاصطناعي"
- copy: "نسخ"
- context: "التفاعلات المراد مشاركتها:"
- share_tip: "بدلًا من ذلك، يمكنك مشاركة المحادثة بأكملها"
- bot_names:
- fake: "Fake Test Bot"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "اليوم"
- last_7_days: "آخر 7 أيام"
- last_30_days: "آخر 30 يومًا"
- sentiments:
- dashboard:
- title: "المشاعر"
- sentiment_analysis:
- filter_types:
- all: "الكل"
- positive: "إيجابية"
- neutral: "محايدة"
- negative: "سلبية"
- group_types:
- category: "الفئة"
- tag: "الوسم"
- table:
- sentiment: "المشاعر"
- total_count: "الإجمالي"
- summarization:
- chat:
- title: "تلخيص الرسائل"
- description: "حدِّد خيارًا أدناه لتلخيص المحادثة المُرسَلة خلال الإطار الزمني المطلوب."
- summarize: "تلخيص"
- since:
- zero: "آخر %{count} ساعة"
- one: "آخر ساعة"
- two: "آخر ساعتين (%{count})"
- few: "آخر %{count} ساعات"
- many: "آخر %{count} ساعة"
- other: "آخر %{count} ساعة"
- topic:
- title: "ملخص الموضوع"
- close: "إغلاق لوحة الملخص"
- topic_list_layout:
- button:
- compact: "مضغوط"
- expanded: "موسَّع"
- expanded_description: "مع ملخصات الذكاء الاصطناعي"
- discobot_discoveries:
- regular_results: "الموضوعات"
- collapse: "طي"
- tooltip:
- actions:
- disable: "إيقاف"
- review:
- types:
- reviewable_ai_post:
- title: "منشور تم الإبلاغ عنه بواسطة الذكاء الاصطناعي"
- reviewable_ai_chat_message:
- title: "رسالة دردشة تم الإبلاغ عنها بواسطة الذكاء الاصطناعي"
diff --git a/config/locales/client.be.yml b/config/locales/client.be.yml
deleted file mode 100644
index b443082a..00000000
--- a/config/locales/client.be.yml
+++ /dev/null
@@ -1,144 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-be:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "сартаваць па"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Номер тэмы"
- title:
- label: "Загаловак"
- categories:
- label: "катэгорыі"
- tags:
- label: "тэгі"
- llm_triage:
- fields:
- category:
- label: "катэгорыя"
- tags:
- label: "тэгі"
- canned_reply:
- label: "Адказаць"
- discourse_ai:
- features:
- back: "Назад"
- disabled: "(адключана)"
- groups: "групы:"
- edit: "Рэдагаваць"
- filters:
- all: "Усё"
- reset: "скінуць"
- search:
- name: "Пошук"
- spam:
- name: "Спам"
- spam:
- short_title: "Спам"
- enable: "Уключыць"
- test_modal:
- spam: "Спам"
- usage:
- summary: "вынік"
- username: "Імя карыстальніка"
- total_requests: "Усяго запытаў"
- ai_persona:
- back: "Назад"
- name: "імя"
- edit: "рэдагаваць"
- export: "экспарт"
- description: "Апісанне"
- user: карыстальнік
- save: "Захаваць"
- delete: "выдаляць"
- response_format:
- open_modal: "рэдагаваць"
- filters:
- reset: "скінуць"
- rag:
- uploads:
- title: "Загрузкі"
- uploading: "Запампоўванне ..."
- tools:
- back: "Назад"
- export: "экспарт"
- name: "імя"
- description: "Апісанне"
- summary: "вынік"
- save: "Захаваць"
- remove_parameter: "выдаліць"
- parameter_required: "абавязковыя"
- edit: "Рэдагаваць"
- delete: "Выдаліць"
- llms:
- display_name: "імя"
- save: "Захаваць"
- edit: "рэдагаваць"
- back: "Назад"
- delete: выдаляць
- quotas:
- group: "група"
- usage:
- ai_spam: "Спам"
- next:
- title: "Далей"
- tests:
- success: "поспех!"
- providers:
- google: "Google"
- ai_helper:
- context_menu:
- cancel: "адмяніць"
- post_options_menu:
- close: "зачыніць"
- copy: "капіяваць"
- cancel: "адмяніць"
- image_caption:
- save_caption: "захаваць"
- automatic_caption_dialog:
- confirm: "Уключыць"
- embeddings:
- back: "Назад"
- save: "захаваць"
- delete: "Выдаліць"
- edit: "Рэдагаваць"
- tests:
- success: "поспех!"
- display_name: "Імя"
- providers:
- google: "Google"
- ai_bot:
- debug_ai_modal:
- request: "запыт"
- next_log: "Далей"
- share_modal:
- copy: "капіяваць"
- conversations:
- today: "сёння"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Усё"
- neutral: "нейтральны"
- group_types:
- category: "катэгорыя"
- table:
- total_count: "агульны"
- discobot_discoveries:
- regular_results: "тэмы"
- tooltip:
- actions:
- disable: "Адключыць"
diff --git a/config/locales/client.bg.yml b/config/locales/client.bg.yml
deleted file mode 100644
index 4e3b03eb..00000000
--- a/config/locales/client.bg.yml
+++ /dev/null
@@ -1,175 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-bg:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Сортирай по"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Тема ID "
- title:
- label: "Заглавие"
- categories:
- label: "Категории"
- tags:
- label: "Тагове"
- llm_triage:
- fields:
- category:
- label: "Категория"
- tags:
- label: "Тагове"
- canned_reply:
- label: "Отговорете"
- discourse_ai:
- features:
- back: "Назад"
- disabled: "(деактивирани)"
- groups: "Групи:"
- no_persona: "Не е зададено"
- no_groups: "Без"
- edit: "Редактирай"
- expand_list:
- one: "(още %{count})"
- other: "(още %{count})"
- collapse_list: "(покажи по-малко)"
- filters:
- all: "Всички"
- reset: "Нулиране"
- search:
- name: "Търсене"
- spam:
- name: "Спам"
- modals:
- select_option: "Изберете опция..."
- spam:
- short_title: "Спам"
- last_seven_days: "Последните 7 дни"
- enable: "Позволи"
- test_modal:
- spam: "Спам"
- usage:
- summary: "Сумарно"
- username: "Потребителско име"
- total_requests: "Общо заявки"
- periods:
- last_day: "Последните 24 часа"
- custom: "Ръчно задаване..."
- ai_persona:
- back: "Назад"
- name: "Име"
- edit: "Редактирай"
- export: "Експорт "
- description: "Описание"
- user: Потребител
- save: "Запази "
- enabled: "Да е включен?"
- delete: "Изтрий"
- response_format:
- open_modal: "Редактирай"
- modal:
- key_title: "Ключ"
- filters:
- reset: "Нулиране"
- rag:
- uploads:
- uploading: "Качва се..."
- tools:
- back: "Назад"
- export: "Експорт "
- name: "Име"
- description: "Описание"
- summary: "Сумарно"
- save: "Запази "
- remove_parameter: "Премахване"
- parameter_required: "Задъжителни"
- edit: "Редактирай"
- delete: "Изтрий"
- llms:
- display_name: "Име"
- save: "Запази "
- edit: "Редактирай"
- back: "Назад"
- delete: Изтрий
- quotas:
- group: "Група"
- max_usages: "Максимум използвания"
- duration: "Продължителност"
- durations:
- hour: "1 час"
- six_hours: "6 часа"
- day: "24 часа"
- custom: "Ръчно задаване..."
- hours: "часа"
- usage:
- ai_summarization: "Обобщаване"
- ai_spam: "Спам"
- next:
- title: "Напред"
- providers:
- google: "Google"
- fake: "По избор"
- ai_helper:
- context_menu:
- cancel: "Прекрати"
- discard: "Отхвърляне"
- post_options_menu:
- close: "Затвори"
- copy: "Копирай"
- copied: "Копирано!"
- cancel: "Прекрати"
- image_caption:
- save_caption: "Запази "
- automatic_caption_dialog:
- confirm: "Позволи"
- embeddings:
- back: "Назад"
- save: "Запази "
- delete: "Изтрий"
- edit: "Редактирай"
- display_name: "Име "
- providers:
- google: "Google"
- fake: "По избор"
- ai_bot:
- debug_ai_modal:
- request: "Заявка"
- next_log: "Напред"
- previous_log: "Предишни"
- invite_ai_conversation:
- button: "Покана"
- share_modal:
- copy: "Копирай"
- conversations:
- today: "Днес"
- last_7_days: "Последните 7 дни"
- last_30_days: "Последните 30 дни"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Всички"
- group_types:
- category: "Категория"
- table:
- total_count: "Общо"
- summarization:
- chat:
- summarize: "Обобщаване"
- discobot_discoveries:
- regular_results: "Теми"
- collapse: "Намали"
- tooltip:
- actions:
- disable: "Деактивиране"
diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml
deleted file mode 100644
index 7e752147..00000000
--- a/config/locales/client.bs_BA.yml
+++ /dev/null
@@ -1,163 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-bs_BA:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Sortiraj po"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID teme"
- title:
- label: "Naslov"
- categories:
- label: "Kategorije"
- tags:
- label: "Oznake"
- llm_triage:
- fields:
- category:
- label: "Kategorija"
- tags:
- label: "Oznake"
- canned_reply:
- label: "Odgovori"
- discourse_ai:
- features:
- back: "Prethodno"
- disabled: "(neomogućen)"
- groups: "Groups:"
- no_groups: "Ništa"
- edit: "Edit"
- expand_list:
- one: "(još %{count})"
- few: "(još %{count})"
- other: "(još %{count})"
- filters:
- all: "All"
- reset: "Resetovati"
- search:
- name: "Pretraži"
- spam:
- name: "Spam"
- modals:
- select_option: "Odaberi opciju..."
- spam:
- short_title: "Spam"
- enable: "Omogući"
- test_modal:
- spam: "Spam"
- usage:
- summary: "Sažetak"
- username: "Nadimak"
- ai_persona:
- back: "Prethodno"
- name: "Ime"
- edit: "Edit"
- export: "Izvoz"
- description: "Opis"
- user: User
- save: "Save"
- enabled: "Omogućen?"
- allowed_groups: "Dozvoljene grupe"
- delete: "Delete"
- response_format:
- open_modal: "Edit"
- modal:
- key_title: "Ključ"
- filters:
- reset: "Resetovati"
- rag:
- uploads:
- title: "Uploads"
- uploading: "Učitavanje..."
- tools:
- back: "Prethodno"
- export: "Izvoz"
- name: "Ime"
- description: "Opis"
- summary: "Sažetak"
- save: "Save"
- remove_parameter: "Ukloni"
- parameter_required: "Required"
- edit: "Edit"
- delete: "Delete"
- llms:
- display_name: "Ime"
- save: "Save"
- edit: "Edit"
- back: "Prethodno"
- delete: Delete
- quotas:
- group: "Grupa"
- duration: "Trajanje"
- usage:
- ai_spam: "Spam"
- next:
- title: "Iduće"
- tests:
- success: "Uspjeh!"
- providers:
- google: "Google"
- fake: "Custom"
- ai_helper:
- context_menu:
- cancel: "Odustani"
- discard: "Odbaci"
- post_options_menu:
- close: "Zatvori"
- copy: "Copy"
- copied: "Kopirano!"
- cancel: "Odustani"
- image_caption:
- save_caption: "Save"
- automatic_caption_dialog:
- confirm: "Omogući"
- embeddings:
- back: "Prethodno"
- save: "Save"
- delete: "Delete"
- edit: "Edit"
- tests:
- success: "Uspjeh!"
- display_name: "Ime"
- providers:
- google: "Google"
- fake: "Custom"
- ai_bot:
- debug_ai_modal:
- request: "Zatraži"
- response: "Odgovor"
- next_log: "Iduće"
- previous_log: "Previous"
- invite_ai_conversation:
- button: "Invite"
- share_modal:
- copy: "Copy"
- conversations:
- today: "Today"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "All"
- group_types:
- category: "Kategorija"
- table:
- total_count: "Suma"
- discobot_discoveries:
- regular_results: "Topics"
- collapse: "Spusti"
- tooltip:
- actions:
- disable: "Onemogući"
diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml
deleted file mode 100644
index 709d52ac..00000000
--- a/config/locales/client.ca.yml
+++ /dev/null
@@ -1,170 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ca:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Ordena per"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID de tema"
- title:
- label: "Títol"
- categories:
- label: "Categories"
- tags:
- label: "Etiquetes"
- llm_triage:
- fields:
- category:
- label: "Categoria"
- tags:
- label: "Etiquetes"
- canned_reply:
- label: "Respon"
- discourse_ai:
- features:
- back: "Enrere"
- disabled: "(desactivat)"
- groups: "Grups:"
- no_groups: "Cap"
- edit: "Edita"
- expand_list:
- one: "(%{count} més)"
- other: "(%{count} més)"
- filters:
- all: "Tot"
- reset: "Restableix"
- search:
- name: "Cerca"
- spam:
- name: "Brossa"
- modals:
- select_option: "Trieu una opció..."
- spam:
- short_title: "Brossa"
- last_seven_days: "Els darrers 7 dies"
- enable: "Activa"
- test_modal:
- spam: "Brossa"
- usage:
- summary: "Resum"
- username: "Nom d'usuari "
- total_requests: "Total de peticions"
- periods:
- last_day: "Últimes 24 hores"
- ai_persona:
- back: "Enrere"
- name: "Nom"
- edit: "Edita"
- export: "Exporta"
- description: "Descripció"
- user: Usuari
- save: "Desa"
- enabled: "Activat?"
- delete: "Suprimeix"
- response_format:
- open_modal: "Edita"
- modal:
- key_title: "Clau"
- filters:
- reset: "Restableix"
- rag:
- uploads:
- title: "Càrregues"
- uploading: "Carregant..."
- tools:
- back: "Enrere"
- export: "Exporta"
- name: "Nom"
- description: "Descripció"
- summary: "Resum"
- save: "Desa"
- remove_parameter: "Elimina"
- parameter_required: "Necessari"
- edit: "Edita"
- delete: "Suprimeix"
- llms:
- display_name: "Nom"
- save: "Desa"
- edit: "Edita"
- back: "Enrere"
- delete: Suprimeix
- quotas:
- group: "Grup"
- duration: "Duració"
- hours: "hores"
- usage:
- ai_spam: "Brossa"
- next:
- title: "Següent"
- tests:
- success: "Èxit!"
- providers:
- google: "Google"
- fake: "Personalitzat"
- ai_helper:
- context_menu:
- cancel: "Cancel·la"
- confirm: "Confirma"
- discard: "Descarta"
- post_options_menu:
- close: "Tanca"
- copy: "Còpia"
- copied: "Copiat!"
- cancel: "Cancel·la"
- image_caption:
- save_caption: "Desa"
- automatic_caption_dialog:
- confirm: "Activa"
- embeddings:
- back: "Enrere"
- save: "Desa"
- delete: "Suprimeix"
- edit: "Edita"
- tests:
- success: "Èxit!"
- display_name: "Nom"
- providers:
- google: "Google"
- fake: "Personalitzat"
- ai_bot:
- debug_ai_modal:
- request: "Sol·licita"
- response: "Reacció"
- next_log: "Següent"
- previous_log: "Previ"
- invite_ai_conversation:
- button: "Convida"
- share_modal:
- copy: "Còpia"
- conversations:
- today: "Avui"
- last_7_days: "Els darrers 7 dies"
- last_30_days: "Els darrers 30 dies"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Tot"
- neutral: "Neutre"
- group_types:
- category: "Categoria"
- table:
- total_count: "Total"
- discobot_discoveries:
- regular_results: "Temes"
- collapse: "Redueix"
- tooltip:
- actions:
- disable: "Desactiva"
diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml
deleted file mode 100644
index e85012ec..00000000
--- a/config/locales/client.cs.yml
+++ /dev/null
@@ -1,339 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-cs:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Řadit podle"
- tag:
- label: "Štítek"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID tématu"
- title:
- label: "Nadpis"
- categories:
- label: "Kategorie"
- tags:
- label: "Tagy"
- llm_triage:
- fields:
- category:
- label: "Kategorie"
- tags:
- label: "Tagy"
- canned_reply:
- label: "Odpověď"
- discourse_ai:
- features:
- back: "Zpět"
- disabled: "(vypnuto)"
- groups: "Skupiny:"
- no_persona: "Nenastaveno"
- no_groups: "Žádná"
- edit: "Upravit"
- expand_list:
- one: "(%{count} další)"
- few: "(%{count} další)"
- many: "(%{count} další)"
- other: "(%{count} další)"
- collapse_list: "(zobrazit méně)"
- filters:
- all: "Vše"
- reset: "obnovit výchozí"
- search:
- name: "Vyhledávání"
- ai_helper:
- proofread: Korektura textu
- explain: "Vysvětlit"
- smart_dates: "Chytré datumy"
- markdown_tables: "Generovat Markdown tabulku"
- custom_prompt: "Vlastní pokyn"
- spam:
- name: "Spam"
- modals:
- select_option: "Zvolit možnost..."
- spam:
- short_title: "Spam"
- last_seven_days: "Posledních 7 dní"
- enable: "Zapnout"
- test_modal:
- spam: "Spam"
- usage:
- summary: "Souhrn"
- username: "Uživatelské jméno"
- total_requests: "Celkem požadavků"
- periods:
- last_day: "Posledních 24 hodin"
- custom: "Vlastní…"
- ai_persona:
- back: "Zpět"
- name: "Jméno"
- edit: "Upravit"
- export: "Export"
- description: "Popis"
- user: Uživatel
- save: "Uložit"
- enabled: "Zapnuto?"
- delete: "Smazat"
- response_format:
- open_modal: "Upravit"
- modal:
- key_title: "Klíč"
- filters:
- reset: "obnovit výchozí"
- rag:
- uploads:
- title: "Nahrané soubory"
- button: "Přidat soubory"
- uploading: "Nahrává se..."
- tools:
- back: "Zpět"
- export: "Export"
- name: "Jméno"
- description: "Popis"
- summary: "Souhrn"
- save: "Uložit"
- remove_parameter: "Smazat"
- parameter_required: "Nezbytnosti"
- edit: "Upravit"
- delete: "Smazat"
- llms:
- display_name: "Jméno"
- save: "Uložit"
- edit: "Upravit"
- back: "Zpět"
- delete: Smazat
- quotas:
- group: "Skupina"
- max_usages: "Max. použití"
- duration: "Doba trvání"
- durations:
- hour: "1 hodina"
- six_hours: "6 hodin"
- day: "24 hodin"
- week: "7 dní"
- custom: "Vlastní…"
- hours: "hodiny"
- usage:
- ai_summarization: "Shrnout"
- ai_spam: "Spam"
- next:
- title: "Další"
- tests:
- success: "Úspěch!"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Přizpůsobené"
- related_topics:
- title: "Související témata"
- pill: "Související"
- ai_helper:
- title: "Navrhněte změny pomocí AI"
- description: "Vyberte jednu z níže uvedených možností a umělá inteligence vám navrhne novou verzi textu."
- selection_hint: "Tip: Před otevřením pomocníka můžete také vybrat část textu a přepsat pouze tuto část."
- suggest: "Navrhnout s AI"
- suggest_errors:
- too_many_tags:
- one: "Můžete mít max. %{count} štítek"
- few: "Můžete mít max. %{count} štítky"
- many: "Můžete mít max. %{count} štítků"
- other: "Můžete mít max. %{count} štítků"
- no_suggestions: "Nejsou k dispozici žádné návrhy"
- missing_content: "Chcete-li generovat návrhy, zadejte nějaký obsah."
- context_menu:
- trigger: "Zeptejte se AI"
- loading: "Umělá inteligence generuje"
- cancel: "Zrušit"
- confirm: "Potvrdit"
- discard: "Zrušit"
- changes: "Navrhované úpravy"
- custom_prompt:
- title: "Vlastní pokyn"
- placeholder: "Napište vlastní pokyn..."
- submit: "Odeslat pokyn"
- translate_prompt: "Přeložit do %{language}"
- post_options_menu:
- trigger: "Zeptejte se AI"
- title: "Zeptejte se AI"
- loading: "Umělá inteligence generuje"
- close: "Zavřít"
- copy: "Kopírovat"
- copied: "Zkopírováno!"
- cancel: "Zrušit"
- insert_footnote: "Přidat poznámku pod čarou"
- footnote_disabled: "Automatické vkládání je zakázáno, klikněte na tlačítko Kopírovat a upravte jej ručně."
- footnote_credits: "Vysvětlení pomocí AI"
- fast_edit:
- suggest_button: "Navrhnout úpravu"
- thumbnail_suggestions:
- title: "Navrhované miniatury"
- select: "Vybrat"
- selected: "Vybrané"
- image_caption:
- button_label: "Popisek pomocí AI"
- generating: "Generování popisku..."
- credits: "Popisek od AI"
- save_caption: "Uložit"
- automatic_caption_setting: "Povolit automatické popisky"
- automatic_caption_loading: "Popisuji obrázky..."
- automatic_caption_dialog:
- prompt: "Tento příspěvek obsahuje obrázky bez popisků. Chcete povolit automatické popisky při nahrávání obrázků? (To lze později změnit ve vašich nastaveních)"
- confirm: "Zapnout"
- cancel: "Již se neptat"
- no_content_error: "Nejprve přidejte obsah, abyste na něm mohli provádět akce AI"
- embeddings:
- back: "Zpět"
- save: "Uložit"
- delete: "Smazat"
- edit: "Upravit"
- tests:
- title: "Spustit test"
- success: "Úspěch!"
- display_name: "Název"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- CDCK: "CDCK"
- fake: "Přizpůsobené"
- semantic_search_results:
- toggle: "Zobrazeno %{count} výsledků nalezených pomocí AI"
- toggle_hidden: "Skryto %{count} výsledků nalezených pomocí AI"
- none: "Je nám líto, naše vyhledávání AI nenalezlo žádná odpovídající témata"
- new: "Stisknutím tlačítka „hledat“ začněte hledat nové výsledky pomocí AI"
- unavailable: "Výsledky AI nejsou k dispozici"
- semantic_search_tooltips:
- results_explanation: "Pokud je tato funkce povolena, budou níže přidány další výsledky vyhledávání pomocí umělé inteligence."
- ai_generated_result: "Výsledek hledání nalezen pomocí AI"
- quick_search:
- suffix: "ve všech tématech a příspěvcích s AI"
- ai_artifact:
- collapse_view_label: "Ukončení celé obrazovky (ESC nebo tlačítko Zpět)"
- ai_bot:
- pm_warning: "Zprávy chatbotů s umělou inteligencí jsou pravidelně monitorovány moderátory."
- cancel_streaming: "Zastavit odpověď"
- default_pm_prefix: "[Nepojmenovaná SZ od AI bota]"
- shortcut_title: "Začněte SZ s AI botem"
- share: "Kopírovat konverzaci s AI"
- conversation_shared: "Konverzace zkopírována"
- debug_ai: "Zobrazit nezpracovaný požadavek a odpověď umělé inteligence"
- sidebar_empty: "Zde se zobrazí historie konverzace s botem."
- debug_ai_modal:
- title: "Zobrazit interakci s umělou inteligencí"
- copy_request: "Kopírovat požadavek"
- copy_response: "Kopírovat odpověď"
- request_tokens: "Tokeny požadavku:"
- response_tokens: "Tokeny odpovědi:"
- request: "Požadavek"
- response: "Odpověď"
- next_log: "Další"
- previous_log: "Předchozí"
- share_full_topic_modal:
- title: "Veřejně sdílet konverzaci"
- share: "Sdílet a kopírovat odkaz"
- update: "Aktualizovat a zkopírovat odkaz"
- delete: "Odstranit sdílení"
- share_ai_conversation:
- name: "Sdílet konverzaci s AI"
- title: "Sdílejte veřejně tuto konverzaci s umělou inteligencí"
- invite_ai_conversation:
- button: "Pozvat"
- title: "Pozvat do konverzace s AI"
- ai_title: "Konverzace s AI"
- share_modal:
- title: "Kopírovat konverzaci s AI"
- copy: "Kopírovat"
- context: "Interakce ke sdílení:"
- share_tip: "Můžete také sdílet celou konverzaci."
- bot_names:
- fake: "Falešný testovací bot"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonet"
- claude-3-haiku: "Haiku Claude 3"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- header: "S čím mohu pomoci?"
- submit: "Odeslat otázku"
- disclaimer: "Generativní umělá inteligence může dělat chyby. Ověřte si důležité informace."
- placeholder: "Zeptejte se..."
- new: "Nová otázka"
- min_input_length_message:
- one: "Zpráva musí mít 1 nebo více znaků"
- few: "Zpráva musí mít %{count} nebo více znaků"
- many: "Zpráva musí mít %{count} nebo více znaků"
- other: "Zpráva musí mít %{count} nebo více znaků"
- messages_sidebar_title: "Konverzace"
- today: "Dnes"
- last_7_days: "Posledních 7 dní"
- last_30_days: "Posledních 30 dní"
- upload_files: "Nahrát soubory"
- sentiments:
- dashboard:
- title: "Sentiment"
- sentiment_analysis:
- filter_types:
- all: "Vše"
- group_types:
- category: "Kategorie"
- tag: "Štítek"
- table:
- sentiment: "Sentiment"
- total_count: "Celkem"
- summarization:
- chat:
- title: "Shrnout zprávy"
- description: "Výběrem níže uvedené možnosti shrnete konverzaci odeslanou v požadovaném časovém období."
- summarize: "Shrnout"
- since:
- one: "Poslední hodina"
- few: "Poslední %{count} hodiny"
- many: "Posledních %{count} hodin"
- other: "Posledních %{count} hodin"
- topic:
- title: "Souhrn tématu"
- close: "Zavřít panel souhrnu"
- topic_list_layout:
- button:
- compact: "Kompaktní"
- expanded: "Rozšířený"
- expanded_description: "se souhrny AI"
- discobot_discoveries:
- regular_results: "Témata"
- collapse: "Sbalit"
- tooltip:
- actions:
- disable: "Vypnout"
diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml
deleted file mode 100644
index 96facb17..00000000
--- a/config/locales/client.da.yml
+++ /dev/null
@@ -1,186 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-da:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Sorter efter"
- tag:
- label: "Mærke"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Emne ID"
- title:
- label: "Titel"
- categories:
- label: "Kategorier"
- tags:
- label: "Mærker"
- llm_triage:
- fields:
- category:
- label: "Kategori"
- tags:
- label: "Mærker"
- canned_reply:
- label: "Svar"
- discourse_ai:
- features:
- back: "Tilbage"
- disabled: "(deaktiveret)"
- groups: "Grupper:"
- no_groups: "Ingen"
- edit: "Rediger"
- expand_list:
- one: "(%{count} mere)"
- other: "(%{count} mere)"
- collapse_list: "(vis mindre)"
- filters:
- all: "Alle"
- reset: "Nulstil"
- search:
- name: "Søg"
- spam:
- name: "Spam"
- modals:
- select_option: "Vælg en indstilling..."
- spam:
- short_title: "Spam"
- last_seven_days: "Seneste 7 dage"
- enable: "Aktiver"
- test_modal:
- result: "Resultat"
- spam: "Spam"
- usage:
- summary: "Statistik"
- username: "Brugernavn"
- total_requests: "Total antal anmodninger"
- periods:
- last_day: "Seneste 24 timer"
- custom: "Tilpasset..."
- ai_persona:
- back: "Tilbage"
- name: "Navn"
- edit: "Rediger"
- export: "Eksporter"
- description: "Beskrivelse"
- user: Bruger
- save: "Gem"
- enabled: "Aktiveret?"
- allowed_groups: "Tilladte grupper"
- delete: "Slet"
- response_format:
- open_modal: "Rediger"
- modal:
- key_title: "Nøgle"
- filters:
- reset: "Nulstil"
- rag:
- uploads:
- title: "Overførsler"
- uploading: "Overfører…"
- tools:
- back: "Tilbage"
- export: "Eksporter"
- name: "Navn"
- description: "Beskrivelse"
- summary: "Statistik"
- save: "Gem"
- remove_parameter: "Fjern"
- parameter_required: "Obligatoriske"
- edit: "Rediger"
- delete: "Slet"
- llms:
- display_name: "Navn"
- save: "Gem"
- edit: "Rediger"
- back: "Tilbage"
- delete: Slet
- quotas:
- group: "Gruppe"
- max_usages: "Maks. anvendelser"
- duration: "Varighed"
- durations:
- hour: "1 time"
- six_hours: "6 timer"
- day: "24 timer"
- week: "7 dage"
- custom: "Tilpasset..."
- hours: "timer"
- usage:
- ai_spam: "Spam"
- next:
- title: "Næste"
- tests:
- success: "Succes!"
- providers:
- google: "Google"
- fake: "Tilpasset"
- ai_helper:
- context_menu:
- cancel: "Annuller"
- confirm: "Bekræft"
- discard: "Kassér"
- post_options_menu:
- close: "Luk"
- copy: "Kopier"
- copied: "Kopieret!"
- cancel: "Annuller"
- thumbnail_suggestions:
- select: "Vælg"
- image_caption:
- save_caption: "Gem"
- automatic_caption_dialog:
- confirm: "Aktiver"
- embeddings:
- back: "Tilbage"
- save: "Gem"
- delete: "Slet"
- edit: "Rediger"
- tests:
- success: "Succes!"
- display_name: "Navn"
- providers:
- google: "Google"
- fake: "Tilpasset"
- ai_bot:
- debug_ai_modal:
- request: "Anmod om medlemskab"
- response: "Svar"
- next_log: "Næste"
- previous_log: "Forrige"
- invite_ai_conversation:
- button: "Invitér"
- share_modal:
- copy: "Kopier"
- conversations:
- today: "I dag"
- last_7_days: "Seneste 7 dage"
- last_30_days: "Seneste 30 dage"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Alle"
- neutral: "Neutral"
- group_types:
- category: "Kategori"
- tag: "Mærke"
- table:
- total_count: "Total"
- discobot_discoveries:
- regular_results: "Emner"
- collapse: "Fold sammen"
- tooltip:
- actions:
- disable: "Deaktiver"
diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml
deleted file mode 100644
index 48ef1221..00000000
--- a/config/locales/client.de.yml
+++ /dev/null
@@ -1,896 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-de:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Ermöglicht KI-Suche"
- stream_completion: "Ermöglicht das Streamen von KI-Persona-Vervollständigungen"
- update_personas: "Ermöglicht die Aktualisierung von KI-Personas"
- site_settings:
- categories:
- discourse_ai: "Discourse-KI"
- dashboard:
- emotion:
- title: "Emotion"
- description: "Die Tabelle listet die Anzahl der Beiträge auf, die mit einer bestimmten Emotion klassifiziert wurden. Klassifiziert mit dem Modell „SamLowe/roberta-base-go_emotions“."
- reports:
- filters:
- group_by:
- label: "Gruppieren nach"
- sort_by:
- label: "Sortieren nach"
- tag:
- label: "Schlagwort"
- logs:
- staff_actions:
- actions:
- create_ai_llm_model: "LLM-Modell erstellen"
- update_ai_llm_model: "LLM-Modell aktualisieren"
- delete_ai_llm_model: "LLM-Modell löschen"
- create_ai_persona: "KI-Persona erstellen"
- update_ai_persona: "KI-Persona aktualisieren"
- delete_ai_persona: "KI-Persona löschen"
- create_ai_tool: "KI-Tool erstellen"
- update_ai_tool: "KI-Tool aktualisieren"
- delete_ai_tool: "KI-Tool löschen"
- create_ai_embedding: "KI-Einbettung erstellen"
- update_ai_embedding: "KI-Einbettung aktualisieren"
- delete_ai_embedding: "KI-Einbettung löschen"
- update_ai_spam_settings: "KI-Spam-Einstellungen aktualisieren"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Absender"
- description: "Der Benutzer, der den Bericht senden wird"
- receivers:
- label: "Empfänger"
- description: "Die Benutzer, die den Bericht erhalten sollen (an E-Mail-Adressen wird eine E-Mail verschickt, Benutzernamen erhalten eine PN)"
- topic_id:
- label: "Themen-ID"
- description: "Die Themen-ID für die Veröffentlichung des Berichts"
- title:
- label: "Titel"
- description: "Der Titel des Berichts"
- days:
- label: "Tage"
- description: "Die Zeitspanne des Berichts"
- offset:
- label: "Offset"
- description: "Wenn du den Bericht zu Testzwecken in der Vergangenheit ausführen möchtest, kannst du den Bericht mit einem Offset zu einem früheren Zeitpunkt starten."
- instructions:
- label: "Anweisungen"
- description: "Die Anweisungen für das große Sprachmodell"
- sample_size:
- label: "Stichprobengröße"
- description: "Die Anzahl der Beiträge, die für den Bericht ausgewählt werden sollen"
- tokens_per_post:
- label: "Token pro Beitrag"
- description: "Die Anzahl der pro Beitrag zu verwendenden LLM-Token"
- model:
- label: "Modell"
- description: "LLM zur Berichtserstellung"
- categories:
- label: "Kategorien"
- description: "Themen nur nach diesen Kategorien filtern"
- tags:
- label: "Schlagwörter"
- description: "Themen nur nach diesen Schlagwörtern filtern"
- exclude_tags:
- label: "Schlagwörter ausschließen"
- description: "Themen mit diesen Schlagwörtern ausschließen"
- exclude_categories:
- label: "Kategorien ausschließen"
- description: "Themen mit diesen Kategorien ausschließen"
- allow_secure_categories:
- label: "Sichere Kategorien zulassen"
- description: "Erlaube, dass der Bericht für Themen in sicheren Kategorien erstellt wird"
- suppress_notifications:
- label: "Benachrichtigungen unterdrücken"
- description: "Unterdrücke Benachrichtigungen, die der Bericht durch die Umwandlung in Inhalt erzeugen kann. Dadurch werden Erwähnungen und interne Links umgewandelt."
- debug_mode:
- label: "Debug-Modus"
- description: "Aktiviere den Debug-Modus, um die Roheingabe und -ausgabe des LLM anzuzeigen"
- priority_group:
- label: "Prioritätsgruppe"
- description: "Inhalte aus dieser Gruppe im Bericht hervorheben"
- temperature:
- label: "Temperatur"
- description: "Temperatur, die für das LLM verwendet werden soll. Erhöhen, um die Zufälligkeit zu erhöhen (leer lassen, um das Standardmodell zu verwenden)"
- top_p:
- label: "Top P"
- description: "Top P für die LLM, erhöhen, um die Zufälligkeit zu erhöhen (leer lassen, um das Standardmodell zu verwenden)"
- llm_tool_triage:
- fields:
- model:
- label: "Modell"
- description: "Das für die Sichtung verwendete Standard-Sprachmodell"
- tool:
- label: "Tool"
- description: "Tool für die Sichtung (das Tool darf keine Parameter definiert haben)"
- llm_persona_triage:
- fields:
- persona:
- label: "Persona"
- description: "KI-Persona, die für die Sichtung verwendet werden soll (Standard-LLM und -Benutzer müssen eingestellt sein)"
- whisper:
- label: "Als Flüstern antworten"
- description: "Ob die Antwort der Persona ein Flüstern sein soll"
- silent_mode:
- label: "Stiller Modus"
- description: "Im stillen Modus empfängt die Persona den Inhalt, schreibt aber nichts in das Forum - nützlich bei der Sichtung mit Tools"
- llm_triage:
- fields:
- system_prompt:
- label: "System-Eingabeaufforderung"
- description: "Die Eingabeaufforderung, die für die Triage verwendet wird. Achte darauf, dass sie mit einem einzigen Wort antwortet, das du zum Auslösen der Aktion verwenden kannst"
- max_post_tokens:
- label: "Max. Beitrags-Token"
- description: "Die maximale Anzahl von Token, die mit LLM-Triage gescannt werden"
- stop_sequences:
- label: "Sequenzen stoppen"
- description: "Weise das Modell an, die Token-Generierung anzuhalten, wenn einer dieser Werte erreicht wird"
- search_for_text:
- label: "Suche nach Text"
- description: "Wenn der folgende Text in der LLM-Antwort erscheint, wende diese Maßnahmen an"
- category:
- label: "Kategorie"
- description: "Kategorie, die auf das Thema anzuwenden ist"
- tags:
- label: "Schlagwörter"
- description: "Schlagwörter, die auf das Thema anzuwenden sind"
- canned_reply:
- label: "Antworten"
- description: "Rohtext der vorgefertigten Antwort auf einen Beitrag zum Thema"
- canned_reply_user:
- label: "Antwortender Benutzer"
- description: "Benutzername des Benutzers, der die vorgefertigte Antwort posten soll"
- hide_topic:
- label: "Thema ausblenden"
- description: "Thema nicht für die Öffentlichkeit sichtbar machen, wenn ausgelöst"
- flag_type:
- label: "Meldungs-Typ"
- description: "Art der Meldung, die auf den Beitrag angewendet werden soll (Spam oder einfach zur Überprüfung anzeigen)"
- flag_post:
- label: "Beitrag melden"
- description: "Meldet den Beitrag (entweder als Spam oder zur Überprüfung)"
- include_personal_messages:
- label: "Persönliche Nachrichten einbeziehen"
- description: "Auch persönliche Nachrichten scannen und sortieren"
- whisper:
- label: "Als Flüstern antworten"
- description: "Ob die Antwort der KI ein Flüstern sein soll"
- reply_persona:
- label: "Antwort Persona"
- description: "KI-Persona, die für Antworten verwendet werden soll (muss Standard-LLM haben), wird gegenüber vorgefertigten Antworten bevorzugt"
- model:
- label: "Modell"
- description: "Für die Triage verwendetes Sprachmodell"
- temperature:
- label: "Temperatur"
- description: "Temperatur, die für das LLM verwendet werden soll. Erhöhen, um die Zufälligkeit zu erhöhen (leer lassen, um das Standardmodell zu verwenden)"
- discourse_ai:
- title: "KI"
- features:
- short_title: "Funktionen"
- description: "Dies sind die KI-Funktionen, die den Besuchern deiner Website zur Verfügung stehen. Sie können so konfiguriert werden, dass sie bestimmte Personas und LLM verwenden, und der Zugriff kann mithilfe von Gruppen gesteuert werden."
- back: "Zurück"
- disabled: "(deaktiviert)"
- persona:
- one: "Persona:"
- other: "Personas:"
- groups: "Gruppen:"
- llm:
- one: "LLM:"
- other: "LLM:"
- no_llm: "Kein LLM ausgewählt"
- no_persona: "Nicht festgelegt"
- no_groups: "Keiner"
- edit: "Bearbeiten"
- expand_list:
- one: "(%{count} weitere)"
- other: "(%{count} weitere)"
- collapse_list: "(weniger anzeigen)"
- bot:
- bot: "Chatbot"
- name: "Bot"
- description: "Ein Chatbot, der Fragen beantworten und Benutzern in persönlichen Nachrichten, im Forum und im Chat helfen kann"
- nav:
- configured: "Konfiguriert"
- unconfigured: "Nicht konfiguriert"
- filters:
- all: "Gesamt"
- text: "Suche nach Funktionen, Personas, LLMs oder Gruppen..."
- no_results: "Es wurden keine Funktionen gefunden, die deinen Filtern entsprechen."
- reset: "Zurücksetzen"
- summarization:
- name: "Zusammenfassungen"
- description: "Stellt eine Schaltfläche für Zusammenfassungen zur Verfügung, mit der Besucher Themen zusammenfassen können"
- topic_summaries: "Themenzusammenfassungen"
- gists: "Kurze Zusammenfassungen der in Themenliste"
- search:
- name: "Suche"
- description: "Verbessert das Sucherlebnis durch KI-generierte Antworten auf Suchanfragen"
- discoveries: "Entdeckungen"
- embeddings:
- name: "Einbettungen"
- description: "Ermöglicht Funktionen wie „Verwandte Themen“ und „KI-Suche“ durch die Erstellung semantischer Textdarstellungen"
- hyde: "HyDE"
- discord:
- name: "Discord-Integration"
- description: "Fügt die Möglichkeit hinzu, Discord-Kanäle zu durchsuchen"
- search: "Discord-Suche"
- inference:
- name: "Abgeleitete Konzepte"
- description: "Ordnet Themen und Beiträge in Interessensgebiete / Labels."
- generate_concepts: "Konzepterkennung"
- match_concepts: "Passende Konzepte"
- deduplicate_concepts: "Deduplizierung von Konzepten"
- ai_helper:
- name: "Helfer"
- description: "Unterstützt Benutzer bei der Interaktion in der Community, z. B. beim Erstellen von Themen, Verfassen von Beiträgen und Lesen von Inhalten"
- proofread: Text korrekturlesen
- title_suggestions: "Titel vorschlagen"
- explain: "Erklären"
- illustrate_post: "Beitrag illustrieren"
- smart_dates: "Intelligente Termine"
- translate: "Übersetzen"
- markdown_tables: "Markdown-Tabelle generieren"
- custom_prompt: "Benutzerdefinierte Eingabeaufforderung"
- image_caption: "Bildbeschriftungen"
- translator: "Übersetzer"
- translation:
- name: "Übersetzen"
- description: "Übersetzt Inhalte in unterstützte Sprachen"
- locale_detector: "Sprachdetektor"
- post_raw_translator: "Übersetzer für die Roheingabe des Beitrags"
- topic_title_translator: "Thementitel-Übersetzer"
- short_text_translator: "Kurztext-Übersetzer"
- spam:
- name: "Spam"
- description: "Identifiziert potenziellen Spam mithilfe des ausgewählten LLMs und meldet ihn den Moderatoren der Website in der Warteschlange zur Überprüfung."
- inspect_posts: "Beiträge prüfen"
- modals:
- select_option: "Wähle eine Option aus..."
- layout:
- table: "Tabelle"
- card: "Karte"
- spam:
- short_title: "Spam"
- title: "Spam-Behandlung konfigurieren"
- select_llm: "LLM auswählen"
- select_persona: "Persona auswählen"
- custom_instructions: "Benutzerdefinierte Anweisungen"
- custom_instructions_help: "Benutzerdefinierte Anweisungen speziell für deine Website, die der KI beim Identifizieren von Spam helfen, z. B. „Gehe beim Scannen von Posts, die nicht in englischer Sprache sind, aggressiver vor.“"
- last_seven_days: "Letzte 7 Tage"
- scanned_count: "Gescannte Beiträge"
- false_positives: "Falsch gemeldet"
- false_negatives: "Spam übersehen"
- spam_detected: "Spam erkannt"
- custom_instructions_placeholder: "Websitespezifische Anweisungen für die KI zur genaueren Identifizierung von Spam"
- enable: "Aktivieren"
- spam_tip: "Die KI-Spamerkennung scannt die ersten 3 Beiträge aller neuen Benutzer zu öffentlichen Themen. Sie meldet sie zur Überprüfung und blockiert Benutzer, wenn es sich wahrscheinlich um Spam handelt."
- settings_saved: "Einstellungen gespeichert"
- spam_description: "Identifiziert potenziellen Spam mithilfe des ausgewählten LLMs und meldet ihn den Moderatoren der Website in der Warteschlange zur Überprüfung."
- no_llms: "Keine LLMs verfügbar"
- test_button: "Test …"
- save_button: "Änderungen speichern"
- test_modal:
- title: "Testen der Spam-Erkennung"
- post_url_label: "Beitrags-URL oder -ID"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 oder Beitrags-ID"
- result: "Ergebnis"
- scan_log: "Scan-Protokoll"
- run: "Test ausführen"
- spam: "Spam"
- not_spam: "Kein Spam"
- stat_tooltips:
- incorrectly_flagged: "Inhalte, die der KI-Bot als Spam gemeldet hat und bei denen die Moderatoren anderer Meinung waren"
- missed_spam: "Von der Community als Spam gemeldete und von Moderatoren als solche bestätigte Inhalte, die vom KI-Bot nicht erkannt wurden"
- errors:
- scan_not_admin:
- message: "Warnung: Spam-Scannen wird nicht korrekt funktionieren, da das Spam-Scan-Konto kein Administrator ist"
- action: "Beheben"
- resolved: "Der Fehler wurde behoben!"
- usage:
- short_title: "Verwendung"
- summary: "Zusammenfassung"
- total_tokens: "Token gesamt"
- tokens_over_time: "Token im Laufe der Zeit"
- features_breakdown: "Nutzung pro Funktion"
- feature: "Funktion"
- usage_count: "Nutzungsanzahl"
- model: "Modell"
- models_breakdown: "Nutzung pro Modell"
- users_breakdown: "Nutzung pro Benutzer"
- all_features: "Alle Funktionen"
- all_models: "Alle Modelle"
- username: "Benutzername"
- total_requests: "Gesamte Anfragen"
- request_tokens: "Anfrage-Token"
- response_tokens: "Antwort-Token"
- net_request_tokens: "Token für Netzanfragen"
- cached_tokens: "Zwischengespeicherte Token"
- cached_request_tokens: "Zwischengespeicherte Anfragetoken"
- total_spending: "Geschätzte Kosten"
- no_users: "Keine Benutzernutzungsdaten gefunden"
- no_models: "Keine Modellnutzungsdaten gefunden"
- no_features: "Keine Funktionsnutzungsdaten gefunden"
- subheader_description: "Token sind die Basiseinheiten, die LLM zum Verstehen und Generieren von Text verwenden. Nutzungsdaten können sich auf die Kosten auswirken"
- stat_tooltips:
- total_requests: "Alle Anfragen an LLMs über Discourse"
- total_tokens: "Alle Token, die bei der Abfrage einer LLM verwendet werden"
- request_tokens: "Tokens, die verwendet werden, wenn das LLM versucht zu verstehen, was du sagst"
- response_tokens: "Token, die verwendet werden, wenn die LLM auf deine Aufforderung antwortet"
- cached_tokens: "Zuvor verarbeitete Anfragetoken, die das LLM wiederverwendet, um Leistung und Kosten zu optimieren"
- total_spending: "Kumulierte Kosten aller von den LLM verwendeten Token auf Grundlage der spezifischen Kostenmetriken, die zu den LLM-Konfigurationseinstellungen hinzugefügt wurden"
- periods:
- last_day: "Letzte 24 Stunden"
- last_week: "Letzte Woche"
- last_month: "Letzter Monat"
- custom: "Benutzerdefiniert …"
- ai_persona:
- ai_tools: "Tools"
- tool_strategies:
- all: "Auf alle Antworten anwenden"
- replies:
- one: "Nur auf die erste Antwort anwenden"
- other: "Auf die ersten %{count} Antworten anwenden"
- back: "Zurück"
- name: "Name"
- edit: "Bearbeiten"
- export: "Exportieren"
- import: "Importieren"
- import_error_conflict: "Beim Importieren von %{name} wurde ein Konflikt erkannt. Möchtest du die vorhandene Persona aktualisieren?"
- overwrite: "Überschreiben"
- description: "Beschreibung"
- no_llm_selected: "Kein Sprachmodell ausgewählt"
- use_parent_llm: "Verwende das Personas Sprachmodell"
- max_context_posts: "Max. Kontext-Beiträge"
- max_context_posts_help: "Die maximale Anzahl von Beiträgen, die die KI als Kontext für die Antwort auf einen Nutzer verwenden soll (leer für Standardwert)."
- vision_enabled: Sehen aktiviert
- vision_enabled_help: Wenn diese Funktion aktiviert ist, versucht die KI, die Bilder zu verstehen, die Nutzer im Thema posten (abhängig davon, ob das verwendete Modell Sehen unterstützt). Unterstützt von den neuesten Modellen von Anthropic, Google und OpenAI.
- vision_max_pixels: Unterstützte Bildgröße
- vision_max_pixel_sizes:
- low: Niedrige Qualität – am günstigsten (256 x 256)
- medium: Mittlere Qualität (512 x 512)
- high: Hohe Qualität – am langsamsten (1024 x 1024)
- tool_details: Tool-Details anzeigen
- tool_details_help: Zeigt den Endnutzern Details darüber, welche Tools das Sprachmodell ausgelöst hat.
- mentionable: Erwähnungen zulassen
- mentionable_help: Wenn diese Funktion aktiviert ist, können Nutzer in erlaubten Gruppen diesen Nutzer in Beiträgen erwähnen und die KI wird als diese Persona antworten.
- user: Nutzer
- create_user: Benutzer erstellen
- create_user_help: Du kannst dieser Persona optional einen Nutzer zuordnen. Wenn du das tust, wird die KI diesen Nutzer verwenden, um auf Anfragen zu antworten.
- default_llm: Standard-Sprachmodell
- default_llm_help: Das Standard-Sprachmodell, das für diese Persona verwendet werden soll. Erforderlich, wenn du die Persona in öffentlichen Beiträgen erwähnen möchtest.
- question_consolidator_llm: Sprachmodell für Fragenkonsolidierer
- question_consolidator_llm_help: Das Sprachmodell, das für den Fragenkonsolidierer verwendet werden soll. Du kannst ein weniger leistungsfähiges Modell wählen, um Kosten zu sparen.
- system_prompt: System-Eingabeaufforderung
- forced_tool_strategy: Erzwungene Tool-Strategie
- allow_chat_direct_messages: "Chat-Direktnachrichten zulassen"
- allow_chat_direct_messages_help: "Wenn aktiviert, können Benutzer in zulässigen Gruppen Direktnachrichten an diese Person senden."
- allow_chat_channel_mentions: "Chat-Kanal-Erwähnungen zulassen"
- allow_chat_channel_mentions_help: "Wenn aktiviert, können Benutzer in zulässigen Gruppen diese Persona in Chatkanälen erwähnen."
- allow_personal_messages: "Persönliche Nachrichten zulassen"
- allow_personal_messages_help: "Wenn aktiviert, können Benutzer in zulässigen Gruppen persönliche Nachrichten an diese Persona senden."
- allow_topic_mentions: "Themenerwähnungen zulassen"
- allow_topic_mentions_help: "Wenn aktiviert, können Benutzer in zulässigen Gruppen diese Persona in Themen erwähnen."
- force_default_llm: "Immer das Standard-Sprachmodell verwenden"
- save: "Speichern"
- saved: "Persona gespeichert"
- enabled: "Aktiviert?"
- tools: "Aktivierte Tools"
- forced_tools: "Erzwungene Tools"
- allowed_groups: "Zulässige Gruppen"
- confirm_delete: "Bist du sicher, dass du diese Persona löschen willst?"
- new: "Neue Persona"
- no_personas: "Du hast noch keine Personas erstellt"
- title: "Personas"
- short_title: "Personas"
- delete: "Löschen"
- temperature: "Temperatur"
- temperature_help: "Temperatur, die für das LLM verwendet werden soll. Erhöhen, um die Kreativität zu steigern (leer lassen, um den Standardwert des Modells zu verwenden, im Allgemeinen ein Wert zwischen 0,0 und 2,0)"
- top_p: "Top P"
- top_p_help: "Top P für das LLM. Erhöhen, um die Zufälligkeit zu steigern (leer lassen, um den Standardwert des Modells zu verwenden, in der Regel ein Wert zwischen 0,0 und 1,0)"
- priority: "Priorität"
- priority_help: "Personas mit Priorität werden den Benutzern am Anfang der Persona-Liste angezeigt. Wenn mehrere Personas Priorität haben, werden sie alphabetisch sortiert."
- tool_options: "Tool-Optionen"
- rag_conversation_chunks: "Unterhaltungs-Chunks durchsuchen"
- rag_conversation_chunks_help: "Die Anzahl der Chunks, die für die RAG-Modell-Suche verwendet werden. Erhöhen, um die Menge des Kontexts zu steigern, den die KI verwenden kann."
- persona_description: "Personas sind eine leistungsstarke Funktion, mit der du das Verhalten der KI-Engine in deinem Discourse-Forum anpassen kannst. Sie fungieren als „Systemnachricht“, welche die Antworten und Interaktionen der KI steuert und dazu beiträgt, ein persönlicheres und ansprechenderes Erlebnis für Benutzer zu schaffen."
- response_format:
- title: "JSON-Antwortformat"
- no_format: "Kein JSON-Format angegeben"
- open_modal: "Bearbeiten"
- modal:
- root_title: "Antwortstruktur"
- key_title: "Schlüssel"
- examples:
- title: Beispiele
- examples_help: Simuliere frühere Interaktionen mit dem LLM und erde es, um bessere Ergebnisse zu erzielen.
- new: Neues Beispiel
- remove: Beispiel löschen
- collapsable_title: "Beispiel #%{number}"
- user: "Nachricht des Benutzers"
- model: "Antwort des Modells"
- list:
- enabled: "KI-Bot?"
- ai_bot:
- title: "KI-Bot-Optionen"
- save_first: "Weitere KI-Bot-Optionen werden verfügbar, sobald du die Persona gespeichert hast."
- filters:
- text: "Finde eine Persona"
- reset: "Zurücksetzen"
- no_results: "Es wurden keine Personas gefunden, die deinen Filtern entsprechen."
- all_features: "Jede Funktion"
- features_list:
- one: "Funktion:"
- other: "Funktionen:"
- llms_list: "LLM:"
- rag:
- title: "RAG"
- options:
- rag_chunk_tokens: "Chunk-Token hochladen"
- rag_chunk_tokens_help: "Die Anzahl der Token, die für jeden Chunk im RAG-Modell verwendet werden. Erhöhen, um die Menge des Kontexts zu steigern, den die KI verwenden kann. (Eine Änderung führt zu einer Neuindizierung aller Uploads.)"
- rag_chunk_overlap_tokens: "Chunk-Überlappungs-Token hochladen"
- rag_chunk_overlap_tokens_help: "Die Anzahl der Token, die sich zwischen den Chunks im RAG-Modell überlappen sollen. (Eine Änderung führt zu einer Neuindizierung aller Uploads.)"
- rag_llm_model: "Indizierungssprachmodell"
- rag_llm_model_help: "Das für die OCR bei der Indizierung von PDFs und Bildern verwendete Sprachmodell"
- show_indexing_options: "Upload-Optionen anzeigen"
- hide_indexing_options: "Upload-Optionen ausblenden"
- uploads:
- title: "Uploads"
- description: "PDF (.pdf), Klartext (.txt) oder Markdown (.md)"
- description_with_images: "Klartext (.txt), Markdown (.md), PDF (.pdf) oder Bild (.png, .jpeg)"
- button: "Dateien hinzufügen"
- filter: "Uploads filtern"
- indexed: "Indiziert"
- indexing: "Indizierung"
- uploaded: "Bereit zur Indizierung"
- uploading: "Wird hochgeladen …"
- remove: "Upload entfernen"
- tools:
- back: "Zurück"
- short_title: "Tools"
- export: "Exportieren"
- import: "Importieren"
- import_error_conflict: "Tool existiert bereits, möchtest du es aktualisieren?"
- overwrite: "Überschreiben"
- no_tools: "Du hast noch keine Tools erstellt"
- name: "Name"
- name_help: "Der Name wird in der Discourse-Benutzeroberfläche angezeigt und ist die Kurzkennung, die du verwendest, um das Tool in verschiedenen Einstellungen zu finden. Er sollte eindeutig sein (er ist erforderlich)."
- new: "Neues Tool"
- tool_name: "Name des Tools"
- tool_name_help: "Der Werkzeugname wird dem großen Sprachmodell präsentiert. Es ist nicht eindeutig, aber es ist von Person zu Person unterschiedlich. (Persona wird beim Speichern validiert)"
- description: "Beschreibung"
- description_help: "Eine klare Beschreibung des Zwecks des Tools für das Sprachmodell"
- subheader_description: "Tools erweitern die Fähigkeiten von KI-Bots mit benutzerdefinierten JavaScript-Funktionen."
- summary: "Zusammenfassung"
- summary_help: "Zusammenfassung des Zwecks der Tools, die den Endnutzern angezeigt werden soll"
- script: "Skript"
- parameters: "Parameter"
- save: "Speichern"
- parameter_type: "Parametertyp"
- add_parameter: "Parameter hinzufügen"
- remove_parameter: "Entfernen"
- parameter_required: "Erforderlich"
- parameter_enum: "Aufzählung"
- parameter_name: "Parametername"
- parameter_description: "Parameterbeschreibung"
- enum_value: "Aufzählungswert"
- add_enum_value: "Aufzählungswert hinzufügen"
- edit: "Bearbeiten"
- test: "Test ausführen"
- delete: "Löschen"
- saved: "Tool gespeichert"
- confirm_delete: "Bist du sicher, dass du dieses Tool löschen willst?"
- test_modal:
- title: "KI-Tool testen"
- run: "Test ausführen"
- result: "Testergebnis"
- llms:
- short_title: "LLM"
- no_llms: "Noch keine LLM"
- new: "Neues Modell"
- display_name: "Name"
- name: "Modell-ID"
- provider: "Anbieter"
- tokenizer: "Tokenizer"
- max_prompt_tokens: "Kontextfenster"
- max_output_tokens: "Maximale Ausgabetoken"
- url: "URL des Dienstes, der das Modell hostet"
- api_key: "API-Schlüssel des Dienstes, der das Modell hostet"
- enabled_chat_bot: "KI-Bot-Auswahl zulassen"
- vision_enabled: "Sehen aktiviert"
- ai_bot_user: "KI-Bot-Benutzer"
- cost_input: "Eingabekosten"
- cost_cached_input: "Eingabekosten für Zwischengespeichertes"
- cost_output: "Ausgabekosten"
- save: "Speichern"
- edit: "Bearbeiten"
- saved: "LLM-Modell gespeichert"
- back: "Zurück"
- confirm_delete: Bist du sicher, dass du dieses Modell löschen willst?
- delete: Löschen
- seeded_warning: "Dieses Modell ist auf deiner Website vorkonfiguriert und kann nicht bearbeitet werden."
- quotas:
- title: "Nutzungskontingente"
- add_title: "Neues Kontingent erstellen"
- group: "Gruppe"
- max_tokens: "Max. Token"
- max_usages: "Max. Verwendungen"
- duration: "Dauer"
- confirm_delete: "Bist du sicher, dass du dieses Kontingent löschen willst?"
- add: "Kontingent hinzufügen"
- durations:
- hour: "1 Stunde"
- six_hours: "6 Stunden"
- day: "24 Stunden"
- week: "7 Tage"
- custom: "Benutzerdefiniert …"
- hours: "Stunden"
- max_tokens_help: "Maximale Anzahl von Token (Wörtern und Zeichen), die jeder Benutzer in dieser Gruppe innerhalb der angegebenen Dauer verwenden kann. Token sind die Einheiten, die von KI-Modellen zur Textverarbeitung verwendet werden – etwa 1 Token = 4 Zeichen oder 3/4 eines Wortes."
- max_tokens_required: "Muss gesetzt werden, wenn die maximale Nutzung nicht festgelegt ist"
- max_usages_help: "Die maximale Anzahl von Benutzern in dieser Gruppe, die das KI-Modell innerhalb der angegebenen Dauer nutzen können. Dieses Kontingent wird für jeden einzelnen Nutzer verfolgt und nicht für die ganze Gruppe."
- max_usages_required: "Muss gesetzt werden, wenn die maximale Anzahl an Tokens nicht gesetzt ist"
- usage:
- ai_bot: "KI-Bot"
- ai_helper: "Helfer"
- ai_helper_image_caption: "Bildbeschriftungen"
- ai_persona: "Persona (%{persona})"
- ai_summarization: "Zusammenfassen"
- ai_embeddings_semantic_search: "KI-Suche"
- ai_spam: "Spam"
- automation: "Automatisierung (%{persona})"
- in_use_warning:
- one: "Dieses Modell wird derzeit von %{settings} verwendet. Wenn es falsch konfiguriert ist, wird die Funktion nicht wie erwartet funktionieren."
- other: "Dieses Modell wird derzeit verwendet von: %{settings}. Wenn es falsch konfiguriert ist, werden die Funktionen nicht wie erwartet funktionieren. "
- model_description:
- none: "Allgemeine Einstellungen, die für die meisten Sprachmodelle funktionieren"
- anthropic-claude-opus-4-0: "Das intelligenteste Modell von Anthropic"
- anthropic-claude-sonnet-4-0: "Optimales Gleichgewicht zwischen Geschwindigkeit und Kosten"
- anthropic-claude-3-7-sonnet-latest: "Optimales Gleichgewicht zwischen Geschwindigkeit und Kosten (vorherige Generation)"
- anthropic-claude-3-5-haiku-latest: "Schnell und kosteneffizient"
- google-gemini-2-5-pro: "Großes multimodales Modell, das eine Vielzahl von Aufgaben bewältigen kann"
- google-gemini-2-0-flash: "Leicht, schnell und kosteneffizient mit multimodaler Argumentation (vorherige Generation)"
- google-gemini-2-5-flash: "Leicht, schnell und kosteneffizient mit multimodaler Argumentation"
- google-gemini-2-0-flash-lite: "Kosteneffizientes Modell mit niedriger Latenz"
- open_ai-o3: "Das leistungsfähigste Argumentationsmodell von Open AI"
- open_ai-o4-mini: "Erweitertes kosteneffizientes Argumentationsmodell"
- open_ai-gpt-4-1: "Das Vorzeigemodell von Open AI. Es ist gut geeignet für Problemlösungen in verschiedenen Bereichen"
- open_ai-gpt-4-1-mini: "Bietet ein ausgewogenes Verhältnis zwischen Intelligenz, Geschwindigkeit und Kosten, was es zu einem attraktiven Modell für viele Anwendungsfälle macht."
- open_ai-gpt-4-1-nano: "Das schnellste und kostengünstigste GPT-4.1-Modell."
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Effizientes leichtgewichtiges mehrsprachiges Modell"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Leistungsstarkes Mehrzweckmodell"
- mistral-mistral-large-latest: "Das leistungsstärkste Modell von Mistral"
- mistral-pixtral-large-latest: "Das leistungsstärkste Modell von Mistral mit Sichtfunktion"
- open_router-x-ai-grok-3-beta: "Das neueste Modell von xAI"
- open_router-deepseek-deepseek-r1-0528-free: "DeepSeeks neuestes Argumentationsmodell"
- open_router-meta-llama-3-3-70b-instruct: "Leistungsstarkes mehrsprachiges Modell"
- preseeded_model_description: "Vorkonfiguriertes Open-Source-Modell unter Verwendung von %{model}"
- configured:
- title: "Konfigurierte LLMs"
- preconfigured_llms: "Wähle dein LLM"
- preconfigured:
- title_no_llms: "Wähle eine Vorlage aus, um loszulegen"
- title: "Unkonfigurierte LLM-Vorlagen"
- description: "LLMs (Large Language Models) sind KI-Tools, die für Aufgaben wie die Zusammenfassung von Inhalten, die Erstellung von Berichten, die Automatisierung von Kundeninteraktionen und die Erleichterung der Forenmoderation und -einsicht optimiert sind"
- fake: "Manuelle Konfiguration"
- button: "Einrichten"
- next:
- title: "Nächstes"
- tests:
- title: "Test ausführen"
- running: "Test wird aufgeführt …"
- success: "Erfolg!"
- failure: "Beim Versuch, das Modell zu kontaktieren, wurde dieser Fehler zurückgegeben: %{error}"
- hints:
- max_prompt_tokens: "Die maximale Anzahl von Token, die das Modell in einer einzigen Anfrage verarbeiten kann"
- max_output_tokens: "Die maximale Anzahl von Token, die das Modell in einer einzigen Anfrage generieren kann"
- display_name: "Der Name, der verwendet wird, um dieses Modell in der Benutzeroberfläche deiner Website zu referenzieren."
- name: "Wir fügen dies in den API-Aufruf ein, um anzugeben, welches Modell wir verwenden werden"
- vision_enabled: "Wenn diese Funktion aktiviert ist, versucht die KI, Bilder zu verstehen. Dafür wird ein Modell benötigt, das Sehen unterstützt. Verfügbar in den neuesten Modellen von Anthropic, Google und OpenAI."
- enabled_chat_bot: "Wenn diese Option aktiviert ist, können Benutzer dieses Modell auswählen, wenn sie PN mit dem KI-Bot erstellen"
- cost_input: "Die Kosten für die Eingabe pro 1 Million Token für dieses Modell"
- cost_cached_input: "Die Kosten für die Eingabe von Zwischengespeichertem pro 1 Million Token für dieses Modell"
- cost_output: "Die Kosten für die Ausgabe pro 1 Million Token für dieses Modell"
- cost_measure: "$/1M Token"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Benutzerdefiniert"
- provider_fields:
- access_key_id: "AWS-Bedrock-Zugangsschlüssel-ID"
- region: "AWS-Bedrock-Region"
- organization: "Optionale OpenAI-Organisations-ID"
- disable_system_prompt: "Systemmeldung in Eingabeaufforderungen deaktivieren"
- enable_native_tool: "Aktiviere native Tool-Unterstützung"
- disable_native_tools: "Native Tool-Unterstützung deaktivieren (XML-basierte Tools verwenden)"
- provider_order: "Anbieterreihenfolge (kommagetrennte Liste)"
- provider_quantizations: "Reihenfolge der Provider-Quantisierungen (kommagetrennte Liste, z. B.: fp16,fp8)"
- disable_streaming: "Streaming-Vervollständigung deaktivieren (Streaming-Anfragen in Nicht-Streaming-Anfragen umwandeln)"
- reasoning_effort: "Argumentationsaufwand (gilt nur für Argumentationsmodelle)"
- enable_reasoning: "Argumentation aktivieren (nur für Argumentationsmodelle anwendbar)"
- enable_thinking: "Denken aktivieren (nur bei entsprechenden Modellen, z.B.: Flash 2.5)"
- thinking_tokens: "Anzahl der zum Denken verwendeten Token"
- reasoning_tokens: "Anzahl der für die Argumentation verwendeten Token"
- disable_temperature: "Temperatur deaktivieren (einige Denkmodelle unterstützen keine Temperatur)"
- disable_top_p: "Top P deaktivieren (einige Denkmodelle unterstützen Top P nicht)"
- enable_responses_api: "Aktiviere die Antwort-API (bei bestimmten OpenAI-Modellen erforderlich)"
- related_topics:
- title: "Verwandte Themen"
- pill: "Verwandt"
- ai_helper:
- title: "Änderungen mit KI vorschlagen"
- description: "Wähle eine der folgenden Optionen und die KI schlägt dir eine neue Version des Textes vor."
- selection_hint: "Tipp: Du kannst auch einen Teil des Textes auswählen, bevor du den Assistenten öffnest, um nur diesen Teil neu zu schreiben."
- suggest: "Mit KI vorschlagen"
- suggest_errors:
- too_many_tags:
- one: "Du kannst höchstens %{count} Schlagwort haben."
- other: "Du kannst höchstens %{count} Schlagwörter haben."
- no_suggestions: "Keine Vorschläge verfügbar"
- missing_content: "Bitte gib einige Inhalte ein, um Vorschläge zu generieren."
- context_menu:
- trigger: "KI fragen"
- loading: "KI generiert"
- cancel: "Abbrechen"
- regen: "Erneut versuchen"
- confirm: "Bestätigen"
- discard: "Verwerfen"
- changes: "Empfohlene Änderungen"
- custom_prompt:
- title: "Benutzerdefinierte Eingabeaufforderung"
- placeholder: "Benutzerdefinierte Eingabeaufforderung …"
- submit: "Aufforderung senden"
- translate_prompt: "Übersetzen in %{language}"
- post_options_menu:
- trigger: "KI fragen"
- title: "KI fragen"
- loading: "KI generiert"
- close: "Schließen"
- copy: "Kopieren"
- copied: "Kopiert!"
- cancel: "Abbrechen"
- insert_footnote: "Fußnote hinzufügen"
- footnote_disabled: "Automatisches Einfügen deaktiviert. Klicke auf die Schaltfläche „Kopieren“ und bearbeite es manuell"
- footnote_credits: "Erklärung durch KI"
- fast_edit:
- suggest_button: "Bearbeitung vorschlagen"
- thumbnail_suggestions:
- title: "Vorgeschlagene Miniaturansichten"
- select: "Auswählen"
- selected: "Ausgewählt"
- image_caption:
- button_label: "Beschriftung mit KI"
- generating: "Beschriftung wird generiert …"
- credits: "Beschriftet durch KI"
- save_caption: "Speichern"
- automatic_caption_setting: "Automatische Beschriftung einschalten"
- automatic_caption_loading: "Bilder werden beschriftet …"
- automatic_caption_dialog:
- prompt: "Dieser Beitrag enthält nicht beschriftete Bilder. Möchtest du automatische Beschriftungen für hochgeladene Bilder aktivieren? (Das kannst du später in deinen Einstellungen ändern.)"
- confirm: "Aktivieren"
- cancel: "Nicht noch einmal fragen"
- no_content_error: "Füge zuerst Inhalte hinzu, um KI-Aktionen darauf anzuwenden"
- reviewables:
- model_used: "Verwendetes Modell:"
- accuracy: "Genauigkeit:"
- embeddings:
- short_title: "Einbettungen"
- description: "Einbettungen sind numerische Darstellungen von Daten, die Bedeutungen und Beziehungen erfassen und es Discourse-KI-Funktionen wie „Verwandte Themen“ und „KI-Suche“ ermöglichen, Inhalte zu verstehen und zu verknüpfen."
- new: "Neue Einbettung"
- back: "Zurück"
- save: "Speichern"
- saved: "Einbettungskonfiguration gespeichert"
- delete: "Löschen"
- confirm_delete: Bist du sicher, dass du diese Einbettungskonfiguration entfernen möchtest?
- empty: "Du hast noch keine Einbettungen eingerichtet"
- presets: "Wähle eine Voreinstellung aus …"
- configure_manually: "Manuell konfigurieren"
- edit: "Bearbeiten"
- seeded_warning: "Dies ist auf deiner Website vorkonfiguriert und kann nicht bearbeitet werden."
- tests:
- title: "Test ausführen"
- running: "Test wird aufgeführt …"
- success: "Erfolg!"
- failure: "Der Versuch, eine Einbettung zu generieren, ergab: %{error}"
- hints:
- dimensions_warning: "Einmal gespeichert, kann dieser Wert nicht mehr geändert werden."
- matryoshka_dimensions: "Legt die Größe der verschachtelten Einbettungen fest, die zur hierarchischen oder mehrschichtigen Darstellung von Daten verwendet werden, ähnlich wie verschachtelte Puppen ineinander passen."
- embed_prompt: "Präfix für Aufgabenanweisungen beim Generieren von Einbettungen von Foreninhalten. NUR für Einbettungsmodelle, die Präfixe erfordern, wie nomic-embed oder stella. Für die meisten Modelle nicht erforderlich."
- search_prompt: "Präfix für Task-Anweisungen beim Generieren von Einbettungen von Suchanfragen. NUR für Einbettungsmodelle, die Präfixe erfordern, wie nomic-embed oder stella. Für die meisten Modelle nicht erforderlich."
- sequence_length: "Die maximale Anzahl von Token, die bei der Erstellung von Einbettungen oder der Bearbeitung einer Abfrage auf einmal verarbeitet werden können."
- distance_function: "Legt fest, wie die Ähnlichkeit zwischen Einbettungen berechnet wird. Dabei wird entweder der Kosinusabstand (der den Winkel zwischen Vektoren misst) oder das negative innere Produkt (das die Überlappung von Vektorwerten misst) verwendet."
- display_name: "Name"
- provider: "Anbieter"
- url: "URL des Einbettungsdienstes"
- api_key: "API-Schlüssel für den Einbettungsdienst"
- tokenizer: "Tokenizer"
- dimensions: "Einbettungsdimensionen"
- max_sequence_length: "Länge der Sequenz"
- embed_prompt: "Einbettungsaufforderung"
- search_prompt: "Suchaufforderung"
- matryoshka_dimensions: "Matrjoschka-Abmessungen"
- distance_function: "Distanzfunktion"
- distance_functions:
- "<#>": "Negatives inneres Produkt"
- <=>: "Kosinusdistanz"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Benutzerdefiniert"
- provider_fields:
- model_name: "Modellname"
- semantic_search: "Themen (semantisch)"
- semantic_search_loading: "Suche mehr Ergebnisse mithilfe der KI"
- semantic_search_results:
- toggle: "%{count} Ergebnisse, die mit KI gefunden wurden, werden angezeigt"
- toggle_hidden: "%{count} mit KI gefundene Ergebnisse werden ausgeblendet"
- none: "Entschuldigung, unsere KI-Suche hat keine passenden Themen gefunden"
- new: "Drücke auf „Suchen“, um mit der KI nach neuen Ergebnissen zu suchen"
- unavailable: "KI-Ergebnisse nicht verfügbar"
- semantic_search_tooltips:
- results_explanation: "Wenn diese Funktion aktiviert ist, werden zusätzliche KI-Suchergebnisse unten hinzugefügt."
- invalid_sort: "Die Suchergebnisse müssen nach Relevanz sortiert werden, um KI-Ergebnisse anzuzeigen"
- semantic_search_unavailable_tooltip: "Die Suchergebnisse müssen nach Relevanz sortiert werden, um KI-Ergebnisse anzuzeigen"
- ai_generated_result: "Suchergebnis mit KI gefunden"
- quick_search:
- suffix: "in allen Themen und Beiträgen mit KI"
- ai_artifact:
- expand_view_label: "Ansicht erweitern"
- collapse_view_label: "Vollbild verlassen (ESC- oder Zurück-Taste)"
- click_to_run_label: "Artefakt ausführen"
- ai_bot:
- persona: "Persona"
- llm: "Modell"
- pm_warning: "KI-Chatbot-Nachrichten werden regelmäßig von Moderatoren überwacht."
- cancel_streaming: "Antwort abbrechen"
- default_pm_prefix: "[KI-Bot-PN ohne Titel]"
- shortcut_title: "Starte eine PN mit einem KI-Bot"
- share: "KI-Unterhaltung kopieren"
- conversation_shared: "Unterhaltung kopiert"
- embed_copied: "Einbettung in die Zwischenablage kopiert"
- debug_ai: "Rohdaten der KI-Anfrage und -Antwort anzeigen"
- sidebar_empty: "Der Verlauf der Bot-Konversation wird hier angezeigt."
- debug_ai_modal:
- title: "KI-Interaktion ansehen"
- copy_request: "Anfrage kopieren"
- copy_response: "Antwort kopieren"
- request_tokens: "Anfrage-Token:"
- response_tokens: "Antwort-Token:"
- request: "Anfrage"
- response: "Antwort"
- next_log: "Weiter"
- previous_log: "Zurück"
- share_full_topic_modal:
- title: "Unterhaltung öffentlich teilen"
- share: "Link teilen und kopieren"
- update: "Link aktualisieren und kopieren"
- delete: "Freigabe löschen"
- share_ai_conversation:
- name: "KI-Unterhaltung teilen"
- title: "Diese KI-Unterhaltung öffentlich teilen"
- invite_ai_conversation:
- button: "Einladen"
- title: "Zur KI-Konversation einladen"
- ai_label: "KI"
- ai_title: "Unterhaltung mit KI"
- share_modal:
- title: "KI-Unterhaltung kopieren"
- copy: "Kopieren"
- context: "Interaktionen zum Teilen:"
- share_tip: "Alternativ kannst du auch die gesamte Unterhaltung teilen"
- bot_names:
- fake: "Fake-Test-Bot"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonett"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- header: "Wie kann ich helfen?"
- submit: "Frage abschicken"
- disclaimer: "Generative KI kann Fehler machen. Überprüfe wichtige Informationen."
- placeholder: "Stell eine Frage ..."
- new: "Neue Frage"
- min_input_length_message:
- one: "Nachricht muss mindestens 1 Zeichen lang sein"
- other: "Nachricht muss mindestens %{count} Zeichen lang sein"
- messages_sidebar_title: "Unterhaltungen"
- today: "Heute"
- last_7_days: "Letzte 7 Tage"
- last_30_days: "Letzte 30 Tage"
- upload_files: "Dateien hochladen"
- sentiments:
- dashboard:
- title: "Stimmung"
- sidebar:
- overview: "Stimmungsübersicht"
- analysis: "Stimmungsanalyse"
- sentiment_analysis:
- share_chart: "Link zur Tabelle kopieren"
- filter_types:
- all: "Gesamt"
- positive: "Positiv"
- neutral: "Neutral"
- negative: "Negativ"
- group_types:
- category: "Kategorie"
- tag: "Schlagwort"
- table:
- sentiment: "Stimmung"
- total_count: "Gesamt"
- summarization:
- chat:
- title: "Nachrichten zusammenfassen"
- description: "Wähle unten eine Option aus, um die im gewünschten Zeitraum gesendete Unterhaltung zusammenzufassen."
- summarize: "Zusammenfassen"
- since:
- one: "Letzte Stunde"
- other: "Letzte %{count} Stunden"
- topic:
- title: "Zusammenfassung des Themas"
- close: "Zusammenfassungspanel schließen"
- topic_list_layout:
- button:
- compact: "Kompakt"
- expanded: "Erweitert"
- expanded_description: "mit KI-Zusammenfassungen"
- discobot_discoveries:
- main_title: "Discobot Entdeckungen"
- regular_results: "Themen"
- tell_me_more: "Erzähl mir mehr..."
- continue_convo: "Unterhaltung fortsetzen..."
- loading_convo: "Unterhaltung wird geladen"
- collapse: "Zuklappen"
- timed_out: "Discobot konnte keine Entdeckungen finden. Irgendetwas ist schief gelaufen."
- user_setting: "Suchentdeckungen aktivieren"
- tooltip:
- header: "KI-gestützte Suche"
- content: "Natürliche Sprachsuche unterstützt von %{model}"
- actions:
- info: "Wie funktioniert es?"
- disable: "Deaktivieren"
- user_preferences:
- empty: "Zurzeit sind keine relevanten Einstellungen verfügbar"
- review:
- types:
- reviewable_ai_post:
- title: "KI-gemeldeter Beitrag"
- reviewable_ai_chat_message:
- title: "KI-gemeldete Chat-Nachricht"
diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml
deleted file mode 100644
index 1bea01c1..00000000
--- a/config/locales/client.el.yml
+++ /dev/null
@@ -1,215 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-el:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Επιτρέπει την αναζήτηση AI"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- reports:
- filters:
- group_by:
- label: "Ομαδοποίηση κατά"
- sort_by:
- label: "Ταξινόμηση κατά"
- tag:
- label: "Ετικέτα"
- logs:
- staff_actions:
- actions:
- create_ai_llm_model: "Δημιουργία μοντέλου LLM"
- update_ai_llm_model: "Ενημέρωση μοντέλου LLM"
- delete_ai_llm_model: "Διαγραφή μοντέλου LLM"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Αποστολέας"
- description: "Ο χρήστης που θα στείλει την αναφορά"
- receivers:
- label: "Παραλήπτες"
- description: "Οι χρήστες που θα λάβουν την αναφορά (τα emails θα αποσταλούν απευθείας με email, τα ονόματα χρηστών θα αποσταλούν με ΠΜ)"
- topic_id:
- label: "ID Θέματος"
- description: "Το ID θέματος στο οποίο θα δημοσιεύεται η αναφορά"
- title:
- label: "Τίτλος"
- description: "Ο τίτλος της αναφοράς"
- days:
- label: "Ημέρες"
- description: "Το χρονικό διάστημα της αναφοράς"
- instructions:
- label: "Οδηγίες"
- sample_size:
- label: "Μέγεθος δείγματος"
- model:
- label: "Μοντέλο"
- categories:
- label: "Κατηγορίες"
- tags:
- label: "Ετικέτες"
- llm_triage:
- fields:
- category:
- label: "Κατηγορία"
- tags:
- label: "Ετικέτες"
- canned_reply:
- label: "Απάντηση"
- discourse_ai:
- features:
- back: "Πίσω"
- disabled: "(απενεργοποιημένο)"
- groups: "Ομάδες:"
- no_persona: "Δεν έχει οριστεί"
- no_groups: "Κανένα"
- edit: "Επεξεργασία"
- expand_list:
- one: "(%{count} περισσότερο)"
- other: "(%{count} περισσότερα)"
- collapse_list: "(δείξε λιγότερα)"
- filters:
- all: "Όλα"
- reset: "Επαναφορά"
- search:
- name: "Αναζήτηση"
- spam:
- name: "Ανεπιθύμητα"
- modals:
- select_option: "Διαλέξτε μία επιλογή..."
- spam:
- short_title: "Ανεπιθύμητα"
- last_seven_days: "Τελευταίες 7 ημέρες"
- enable: "Ενεργοποίηση"
- test_modal:
- result: "Αποτέλεσμα"
- spam: "Ανεπιθύμητα"
- usage:
- summary: "Περίληψη"
- username: "Όνομα Χρήστη"
- total_requests: "Σύνολο αιτήσεων"
- periods:
- last_day: "Τελευταίες 24 ώρες"
- custom: "Προσαρμοσμένο..."
- ai_persona:
- back: "Πίσω"
- name: "Όνομα"
- edit: "Επεξεργασία"
- export: "Εξαγωγή"
- description: "Περιγραφή"
- user: Χρήστης
- save: "Αποθήκευση"
- enabled: "Ενεργοποιημένο;"
- allowed_groups: "Επιτρεπόμενες ομάδες"
- delete: "Σβήσιμο"
- response_format:
- open_modal: "Επεξεργασία"
- modal:
- key_title: "Κλειδί"
- filters:
- reset: "Επαναφορά"
- rag:
- uploads:
- title: "Μεταφορτώσεις"
- uploading: "Επιφόρτωση..."
- tools:
- back: "Πίσω"
- export: "Εξαγωγή"
- name: "Όνομα"
- description: "Περιγραφή"
- summary: "Περίληψη"
- save: "Αποθήκευση"
- remove_parameter: "Αφαίρεση"
- parameter_required: "Απαιτείται"
- edit: "Επεξεργασία"
- delete: "Σβήσιμο"
- llms:
- display_name: "Όνομα"
- save: "Αποθήκευση"
- edit: "Επεξεργασία"
- back: "Πίσω"
- delete: Σβήσιμο
- quotas:
- group: "Ομάδα"
- max_usages: "Μέγιστες χρήσεις"
- duration: "Διάρκεια"
- durations:
- hour: "1 ώρα"
- six_hours: "6 ώρες"
- day: "24 ώρες"
- week: "7 ημέρες"
- custom: "Προσαρμοσμένο..."
- hours: "ώρες"
- usage:
- ai_spam: "Ανεπιθύμητα"
- next:
- title: "Επόμενο"
- tests:
- success: "Επιτυχία!"
- providers:
- google: "Google"
- fake: "Προσαρμοσμένο"
- ai_helper:
- context_menu:
- cancel: "Ακύρωση"
- discard: "Απόρριψη"
- post_options_menu:
- close: "Κλείσιμο"
- copy: "Αντιγραφή"
- copied: "Αντιγράφηκε!"
- cancel: "Ακύρωση"
- image_caption:
- save_caption: "Αποθήκευση"
- automatic_caption_dialog:
- confirm: "Ενεργοποίηση"
- embeddings:
- back: "Πίσω"
- save: "Αποθήκευση"
- delete: "Σβήσιμο"
- edit: "Επεξεργασία"
- tests:
- success: "Επιτυχία!"
- display_name: "Όνομα"
- providers:
- google: "Google"
- fake: "Προσαρμοσμένο"
- ai_bot:
- debug_ai_modal:
- request: "Αίτημα"
- response: "Απάντηση"
- next_log: "Επόμενο"
- previous_log: "Προηγούμενο"
- invite_ai_conversation:
- button: "Πρόσκληση"
- share_modal:
- copy: "Αντιγραφή"
- conversations:
- today: "Σήμερα"
- last_7_days: "Τελευταίες 7 ημέρες"
- last_30_days: "Τελευταίες 30 ημέρες"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Όλα"
- group_types:
- category: "Κατηγορία"
- table:
- total_count: "Σύνολο"
- discobot_discoveries:
- regular_results: "Θέματα"
- collapse: "Σύμπτυξη"
- tooltip:
- actions:
- disable: "Απενεργοποίηση"
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
deleted file mode 100644
index 5a577657..00000000
--- a/config/locales/client.en.yml
+++ /dev/null
@@ -1,942 +0,0 @@
-en:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Allows AI search"
- stream_completion: "Allows streaming AI persona completions"
- update_personas: "Allows updating AI personas"
-
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "Emotion"
- description: "The table lists a count of posts classified with a determined emotion. Classified with the model 'SamLowe/roberta-base-go_emotions'."
- reports:
- filters:
- group_by:
- label: "Group by"
- sort_by:
- label: "Sort by"
- tag:
- label: "Tag"
- logs:
- staff_actions:
- actions:
- create_ai_llm_model: "Create LLM model"
- update_ai_llm_model: "Update LLM model"
- delete_ai_llm_model: "Delete LLM model"
- create_ai_persona: "Create AI persona"
- update_ai_persona: "Update AI persona"
- delete_ai_persona: "Delete AI persona"
- create_ai_tool: "Create AI tool"
- update_ai_tool: "Update AI tool"
- delete_ai_tool: "Delete AI tool"
- create_ai_embedding: "Create AI embedding"
- update_ai_embedding: "Update AI embedding"
- delete_ai_embedding: "Delete AI embedding"
- update_ai_spam_settings: "Update AI spam settings"
-
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Sender"
- description: "The user that will send the report"
- receivers:
- label: "Receivers"
- description: "The users that will receive the report (emails will be sent direct emails, usernames will be sent a PM)"
- topic_id:
- label: "Topic ID"
- description: "The topic ID to post the report to"
- title:
- label: "Title"
- description: "The title of the report"
- days:
- label: "Days"
- description: "The timespan of the report"
- offset:
- label: "Offset"
- description: "When testing you may want to run the report historically, use offset to start the report in an earlier date"
- instructions:
- label: "Instructions"
- description: "The instructions provided to the large language model"
- sample_size:
- label: "Sample Size"
- description: "The number of posts to sample for the report"
- tokens_per_post:
- label: "Tokens per post"
- description: "The number of LLM tokens to use per post"
- model:
- label: "Model"
- description: "LLM to use for report generation"
- categories:
- label: "Categories"
- description: "Filter topics only to these categories"
- tags:
- label: "Tags"
- description: "Filter topics only to these tags"
- exclude_tags:
- label: "Exclude Tags"
- description: "Exclude topics with these tags"
- exclude_categories:
- label: "Exclude Categories"
- description: "Exclude topics with these categories"
- allow_secure_categories:
- label: "Allow secure categories"
- description: "Allow the report to be generated for topics in secure categories"
- suppress_notifications:
- label: "Suppress Notifications"
- description: "Suppress notifications the report may generate by transforming to content. This will remap mentions and internal links."
- debug_mode:
- label: "Debug Mode"
- description: "Enable debug mode to see the raw input and output of the LLM"
- priority_group:
- label: "Priority Group"
- description: "Prioritize content from this group in the report"
- temperature:
- label: "Temperature"
- description: "Temperature to use for the LLM. Increase to increase randomness (leave empty to use model default)"
- top_p:
- label: "Top P"
- description: "Top P to use for the LLM, increase to increase randomness (leave empty to use model default)"
- persona_id:
- label: "Persona"
- description: "AI Persona to use for report generation"
-
- llm_tool_triage:
- fields:
- model:
- label: "Model"
- description: "The default language model used for triage"
- tool:
- label: "Tool"
- description: "Tool to use for triage (tool must have no parameters defined)"
-
- llm_persona_triage:
- fields:
- persona:
- label: "Persona"
- description: "AI Persona to use for triage (must have default LLM and User set)"
- whisper:
- label: "Reply as Whisper"
- description: "Whether the persona's response should be a whisper"
- silent_mode:
- label: "Silent Mode"
- description: "In silent mode persona will receive the content but will not post anything on the forum - useful when performing triage using tools"
- llm_triage:
- fields:
- system_prompt:
- label: "System Prompt"
- description: "The prompt that will be used to triage, be sure for it to reply with a single word you can use to trigger the action"
- max_post_tokens:
- label: "Max Post Tokens"
- description: "The maximum number of tokens to scan using LLM triage"
- stop_sequences:
- label: "Stop Sequences"
- description: "Instruct the model to halt token generation when arriving at one of these values"
- search_for_text:
- label: "Search for text"
- description: "If the following text appears in the LLM reply, apply these actions"
- category:
- label: "Category"
- description: "Category to apply to the topic"
- tags:
- label: "Tags"
- description: "Tags to apply to the topic"
- canned_reply:
- label: "Reply"
- description: "Raw text of the canned reply to post on the topic"
- canned_reply_user:
- label: "Reply User"
- description: "Username of the user to post the canned reply"
- hide_topic:
- label: "Hide topic"
- description: "Make topic not visible to the public if triggered"
- flag_type:
- label: "Flag type"
- description: "Type of flag to apply to the post (spam or simply raise for review)"
- flag_post:
- label: "Flag post"
- description: "Flags post (either as spam or for review)"
- include_personal_messages:
- label: "Include personal messages"
- description: "Also scan and triage personal messages"
- whisper:
- label: "Reply as Whisper"
- description: "Whether the AI's response should be a whisper"
- reply_persona:
- label: "Reply Persona"
- description: "AI Persona to use for replies (must have default LLM), will be prioritized over canned reply"
- model:
- label: "Model"
- description: "Language model used for triage"
- temperature:
- label: "Temperature"
- description: "Temperature to use for the LLM. Increase to increase randomness (leave empty to use model default)"
- max_output_tokens:
- label: "Max output tokens"
- description: "When specified, sets an upper bound to the maximum number of tokens the model can generate. Respects LLM's max output tokens limit"
-
- discourse_ai:
- title: "AI"
-
- features:
- short_title: "Features"
- description: "These are the AI features available to visitors on your site. These can be configured to use specific personas and LLMs, and can be access controlled by groups."
- back: "Back"
- disabled: "(disabled)"
- persona:
- one: "Persona:"
- other: "Personas:"
- groups: "Groups:"
- llm:
- one: "LLM:"
- other: "LLMs:"
- no_llm: "No LLM selected"
- no_persona: "Not set"
- no_groups: "None"
- edit: "Edit"
- expand_list:
- one: "(%{count} more)"
- other: "(%{count} more)"
- collapse_list: "(show less)"
- bot:
- bot: "Chatbot"
- name: "Bot"
- description: "A chat bot that can answer questions and assist users in personal messages, forum and in chat"
- nav:
- configured: "Configured"
- unconfigured: "Unconfigured"
- filters:
- all: "All"
- text: "Search features, personas, LLMs, or groups..."
- no_results: "No features found matching your filters."
- reset: "Reset"
- summarization:
- name: "Summaries"
- description: "Makes a summarization button available that allows visitors to summarize topics"
- topic_summaries: "Topic summaries"
- gists: "Topic list's short summaries"
- search:
- name: "Search"
- description: "Enhances search experience by providing AI-generated answers to queries"
- discoveries: "Discoveries"
- embeddings:
- name: "Embeddings"
- description: "Powers features like Related Topics and AI Search by generating semantic representations of text"
- hyde: "HyDE"
- discord:
- name: "Discord integration"
- description: "Adds the ability to search Discord channels"
- search: "Discord search"
- inference:
- name: "Inferred concepts"
- description: "Classifies topics and posts into areas of interest / labels."
- generate_concepts: "Concepts inference"
- match_concepts: "Concepts matching"
- deduplicate_concepts: "Concepts deduplication"
-
- ai_helper:
- name: "Helper"
- description: "Assists users in community interaction, such as creating topics, writing posts, and reading content"
- proofread: Proofread text
- title_suggestions: "Suggest titles"
- explain: "Explain"
- illustrate_post: "Illustrate post"
- smart_dates: "Smart dates"
- translate: "Translate"
- markdown_tables: "Generate Markdown table"
- custom_prompt: "Custom prompt"
- image_caption: "Caption images"
- translator: "Translator"
-
- translation:
- name: "Translation"
- description: "Translates content into supported languages"
- locale_detector: "Locale detector"
- post_raw_translator: "Post raw translator"
- topic_title_translator: "Topic title translator"
- short_text_translator: "Short text translator"
-
- spam:
- name: "Spam"
- description: "Identifies potential spam using the selected LLM and flags it for site moderators to inspect in the review queue"
- inspect_posts: "Inspect posts"
-
- modals:
- select_option: "Select an option..."
-
- layout:
- table: "Table"
- card: "Card"
-
- spam:
- short_title: "Spam"
- title: "Configure spam handling"
- select_llm: "Select LLM"
- select_persona: "Select persona"
- custom_instructions: "Custom instructions"
- custom_instructions_help: "Custom instructions specific to your site to help guide the AI in identifying spam, e.g. 'Be more aggressive about scanning posts not in English'."
- last_seven_days: "Last 7 days"
- scanned_count: "Posts scanned"
- false_positives: "Incorrectly flagged"
- false_negatives: "Missed spam"
- spam_detected: "Spam detected"
- custom_instructions_placeholder: "Site-specific instructions for the AI to help identify spam more accurately"
- enable: "Enable"
- spam_tip: "AI spam detection will scan the first 3 posts by all new users on public topics. It will flag them for review and block users if they are likely spam."
- settings_saved: "Settings saved"
- spam_description: "Identifies potential spam using the selected LLM and flags it for site moderators to inspect in the review queue"
- no_llms: "No LLMs available"
- test_button: "Test..."
- save_button: "Save changes"
- test_modal:
- title: "Test spam detection"
- post_url_label: "Post URL or ID"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 or post ID"
- result: "Result"
- scan_log: "Scan log"
- run: "Run test"
- spam: "Spam"
- not_spam: "Not spam"
- stat_tooltips:
- incorrectly_flagged: "Items that the AI bot flagged as spam where moderators disagreed"
- missed_spam: "Items flagged by the community as spam that were not detected by the AI bot, which moderators agreed with"
- errors:
- scan_not_admin:
- message: "Warning: spam scanning will not work correctly because the spam scan account is not an admin"
- action: "Fix"
- resolved: "The error has been resolved!"
-
- usage:
- short_title: "Usage"
- summary: "Summary"
- total_tokens: "Total tokens"
- tokens_over_time: "Tokens over time"
- features_breakdown: "Usage per feature"
- feature: "Feature"
- usage_count: "Usage count"
- model: "Model"
- models_breakdown: "Usage per model"
- users_breakdown: "Usage per user"
- all_features: "All features"
- all_models: "All models"
- username: "Username"
- total_requests: "Total requests"
- request_tokens: "Request tokens"
- response_tokens: "Response tokens"
- net_request_tokens: "Net request tokens"
- cached_tokens: "Cached tokens"
- cached_request_tokens: "Cached request tokens"
- total_spending: "Estimated cost"
- no_users: "No user usage data found"
- no_models: "No model usage data found"
- no_features: "No feature usage data found"
- subheader_description: "Tokens are the basic units that LLMs use to understand and generate text, usage data may affect costs"
- stat_tooltips:
- total_requests: "All requests made to LLMs through Discourse"
- total_tokens: "All the tokens used when prompting an LLM"
- request_tokens: "Tokens used when the LLM tries to understand what you are saying"
- response_tokens: "Tokens used when the LLM responds to your prompt"
- cached_tokens: "Previously processed request tokens that the LLM reuses to optimize performance and cost"
- total_spending: "Cumulative cost of all tokens used by the LLMs based on specified cost metrics added to LLM configuration settings"
- periods:
- last_day: "Last 24 hours"
- last_week: "Last week"
- last_month: "Last month"
- custom: "Custom..."
-
- ai_persona:
- ai_tools: "Tools"
- tool_strategies:
- all: "Apply to all replies"
- replies:
- one: "Apply to first reply only"
- other: "Apply to first %{count} replies"
- back: "Back"
- name: "Name"
- edit: "Edit"
- export: "Export"
- import: "Import"
- import_error_conflict: "Conflict detected importing %{name}, would you like to update the existing persona?"
- overwrite: "Overwrite"
- description: "Description"
- no_llm_selected: "No language model selected"
- use_parent_llm: "Use personas language model"
- max_context_posts: "Max context posts"
- max_context_posts_help: "The maximum number of posts to use as context for the AI when responding to a user. (empty for default)"
- vision_enabled: Vision enabled
- vision_enabled_help: If enabled, the AI will attempt to understand images users post in the topic, depends on the model being used supporting vision. Supported by latest models from Anthropic, Google, and OpenAI.
- vision_max_pixels: Supported image size
- vision_max_pixel_sizes:
- low: Low quality - cheapest (256x256)
- medium: Medium quality (512x512)
- high: High quality - slowest (1024x1024)
- tool_details: Show tool details
- tool_details_help: Will show end users details on which tools the language model has triggered.
- mentionable: Allow mentions
- mentionable_help: If enabled, users in allowed groups can mention this user in posts, the AI will respond as this persona.
- user: User
- create_user: Create user
- create_user_help: You can optionally attach a user to this persona. If you do, the AI will use this user to respond to requests.
- default_llm: Default language model
- default_llm_help: The default language model to use for this persona. Required if you wish to mention persona on public posts.
- question_consolidator_llm: Language Model for Question Consolidator
- question_consolidator_llm_help: The language model to use for the question consolidator, you may choose a less powerful model to save costs.
- system_prompt: System prompt
- forced_tool_strategy: Forced tool strategy
- allow_chat_direct_messages: "Allow chat direct messages"
- allow_chat_direct_messages_help: "If enabled, users in allowed groups can send direct messages to this persona."
- allow_chat_channel_mentions: "Allow chat channel mentions"
- allow_chat_channel_mentions_help: "If enabled, users in allowed groups can mention this persona in chat channels."
- allow_personal_messages: "Allow personal messages"
- allow_personal_messages_help: "If enabled, users in allowed groups can send personal messages to this persona."
- allow_topic_mentions: "Allow topic mentions"
- allow_topic_mentions_help: "If enabled, users in allowed groups can mention this persona in topics."
- force_default_llm: "Always use default language model"
- save: "Save"
- saved: "Persona saved"
- enabled: "Enabled?"
- tools: "Enabled tools"
- forced_tools: "Forced tools"
- allowed_groups: "Allowed groups"
- confirm_delete: "Are you sure you want to delete this persona?"
- new: "New persona"
- no_personas: "You have not created any personas yet"
- title: "Personas"
- short_title: "Personas"
- delete: "Delete"
- temperature: "Temperature"
- temperature_help: "Temperature to use for the LLM. Increase to increase creativity (leave empty to use model default, generally a value from 0.0 to 2.0)"
- top_p: "Top P"
- top_p_help: "Top P to use for the LLM, increase to increase randomness (leave empty to use model default, generally a value from 0.0 to 1.0)"
- priority: "Priority"
- priority_help: "Priority personas are displayed to users at the top of the persona list. If multiple personas have priority, they will be sorted alphabetically."
- tool_options: "Tool options"
- rag_conversation_chunks: "Search conversation chunks"
- rag_conversation_chunks_help: "The number of chunks to use for the RAG model searches. Increase to increase the amount of context the AI can use."
- persona_description: "Personas are a powerful feature that allows you to customize the behavior of the AI engine in your Discourse forum. They act as a 'system message' that guides the AI's responses and interactions, helping to create a more personalized and engaging user experience."
- response_format:
- title: "JSON response format"
- no_format: "No JSON format specified"
- open_modal: "Edit"
- modal:
- root_title: "Response structure"
- key_title: "Key"
- examples:
- title: Examples
- examples_help: Simulate previous interactions with the LLM and ground it to produce better result.
- new: New example
- remove: Delete example
- collapsable_title: "Example #%{number}"
- user: "User message"
- model: "Model response"
-
- list:
- enabled: "AI Bot?"
-
- ai_bot:
- title: "AI bot options"
- save_first: "More AI bot options will become available once you save the persona."
-
- filters:
- text: "Find a persona"
- reset: "Reset"
- no_results: "No personas found matching your filters."
- all_features: "Any feature"
-
- features_list:
- one: "Feature:"
- other: "Features:"
-
- llms_list: "LLM:"
-
- rag:
- title: "RAG"
- options:
- rag_chunk_tokens: "Upload chunk tokens"
- rag_chunk_tokens_help: "The number of tokens to use for each chunk in the RAG model. Increase to increase the amount of context the AI can use. (changing will re-index all uploads)"
- rag_chunk_overlap_tokens: "Upload chunk overlap tokens"
- rag_chunk_overlap_tokens_help: "The number of tokens to overlap between chunks in the RAG model. (changing will re-index all uploads)"
- rag_llm_model: "Indexing Language Model"
- rag_llm_model_help: "The language model used for OCR during indexing of PDFs and images"
- show_indexing_options: "Show upload options"
- hide_indexing_options: "Hide upload options"
- uploads:
- title: "Uploads"
- description: "PDF (.pdf), Plaintext (.txt) or markdown (.md)"
- description_with_images: "Plaintext (.txt), markdown (.md), PDF (.pdf) or image (.png, .jpeg)"
- button: "Add files"
- filter: "Filter uploads"
- indexed: "Indexed"
- indexing: "Indexing"
- uploaded: "Ready to be indexed"
- uploading: "Uploading..."
- remove: "Remove upload"
-
- tools:
- back: "Back"
- short_title: "Tools"
- export: "Export"
- import: "Import"
- import_error_conflict: "Tool already exists, would you like to update it?"
- overwrite: "Overwrite"
- no_tools: "You have not created any tools yet"
- name: "Name"
- name_help: "Name will show up in the Discourse UI and is the short identifier you will use to find the tool in various settings, it should be distinct (it is required)"
- new: "New tool"
- tool_name: "Tool Name"
- tool_name_help: "Tool Name is presented to the large language model. It is not distinct, but it is distinct per persona. (persona validates on save)"
- description: "Description"
- description_help: "A clear description of the tool's purpose for the language model"
- subheader_description: "Tools extend the capabilities of AI bots with user-defined JavaScript functions."
- summary: "Summary"
- summary_help: "Summary of tools purpose to be displayed to end users"
- script: "Script"
- parameters: "Parameters"
- save: "Save"
- parameter_type: "Parameter type"
- add_parameter: "Add parameter"
- remove_parameter: "Remove"
- parameter_required: "Required"
- parameter_enum: "Enum"
- parameter_name: "Parameter name"
- parameter_description: "Parameter description"
- enum_value: "Enum value"
- add_enum_value: "Add enum value"
- edit: "Edit"
- test: "Run test"
- delete: "Delete"
- saved: "Tool saved"
- confirm_delete: "Are you sure you want to delete this tool?"
- test_modal:
- title: "Test AI tool"
- run: "Run test"
- result: "Test result"
-
- llms:
- short_title: "LLMs"
- no_llms: "No LLMs yet"
- new: "New model"
- display_name: "Name"
- name: "Model id"
- provider: "Provider"
- tokenizer: "Tokenizer"
- max_prompt_tokens: "Context window"
- max_output_tokens: "Max output tokens"
- url: "URL of the service hosting the model"
- api_key: "API Key of the service hosting the model"
- enabled_chat_bot: "Allow AI bot selector"
- vision_enabled: "Vision enabled"
- ai_bot_user: "AI bot User"
- cost_input: "Input cost"
- cost_cached_input: "Cached input cost"
- cost_output: "Output cost"
-
- save: "Save"
- edit: "Edit"
- saved: "LLM model saved"
- back: "Back"
- confirm_delete: Are you sure you want to delete this model?
- delete: Delete
- seeded_warning: "This model is pre-configured on your site and cannot be edited."
- quotas:
- title: "Usage quotas"
- add_title: "Create new quota"
- group: "Group"
- max_tokens: "Max tokens"
- max_usages: "Max uses"
- duration: "Duration"
- confirm_delete: "Are you sure you want to delete this quota?"
- add: "Add quota"
- durations:
- hour: "1 hour"
- six_hours: "6 hours"
- day: "24 hours"
- week: "7 days"
- custom: "Custom..."
- hours: "hours"
- max_tokens_help: "Maximum number of tokens (words and characters) that each user in this group can use within the specified duration. Tokens are the units used by AI models to process text - roughly 1 token = 4 characters or 3/4 of a word."
- max_tokens_required: "Must be set if max usages is not set"
- max_usages_help: "Maximum number of times each user in this group can use the AI model within the specified duration. This quota is tracked per individual user, not shared across the group."
- max_usages_required: "Must be set if max tokens is not set"
- usage:
- ai_bot: "AI bot"
- ai_helper: "Helper"
- ai_helper_image_caption: "Image caption"
- ai_persona: "Persona (%{persona})"
- ai_summarization: "Summarize"
- ai_embeddings_semantic_search: "AI search"
- ai_spam: "Spam"
- automation: "Automation (%{persona})"
- in_use_warning:
- one: "This model is currently used by %{settings}. If misconfigured, the feature won't work as expected."
- other: "This model is currently used by the following: %{settings}. If misconfigured, features won't work as expected. "
-
- model_description:
- none: "General settings that work for most language models"
- anthropic-claude-opus-4-0: "Anthropic's most intelligent model"
- anthropic-claude-sonnet-4-0: "Optimal balance of speed and cost"
- anthropic-claude-3-7-sonnet-latest: "Optimal balance of speed and cost (previous generation)"
- anthropic-claude-3-5-haiku-latest: "Fast and cost-effective"
- google-gemini-2-5-pro: "Large multimodal model capable of a wide range of tasks"
- google-gemini-2-0-flash: "Lightweight, fast, and cost-efficient with multimodal reasoning (previous generation)"
- google-gemini-2-5-flash: "Lightweight, fast, and cost-efficient with multimodal reasoning"
- google-gemini-2-0-flash-lite: "Cost efficient and low latency model"
- open_ai-o3: "Open AI's most capable reasoning model"
- open_ai-o4-mini: "Advanced Cost-efficient reasoning model"
- open_ai-gpt-4-1: "Open AI's flagship model. It is well suited for problem solving across domains"
- open_ai-gpt-4-1-mini: "Provides a balance between intelligence, speed, and cost that makes it an attractive model for many use cases."
- open_ai-gpt-4-1-nano: "Fastest, most cost-effective GPT-4.1 model."
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Efficient lightweight multilingual model"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Powerful multipurpose model"
- mistral-mistral-large-latest: "Mistral's most powerful model"
- mistral-pixtral-large-latest: "Mistral's most powerful vision capable model"
- open_router-x-ai-grok-3-beta: "xAI's latest model"
- open_router-deepseek-deepseek-r1-0528-free: "DeepSeek's latest reasoning model"
- open_router-meta-llama-3-3-70b-instruct: "Highly capable multilingual model"
-
- preseeded_model_description: "Pre-configured open-source model utilizing %{model}"
-
- configured:
- title: "Configured LLMs"
- preconfigured_llms: "Select your LLM"
- preconfigured:
- title_no_llms: "Select a template to get started"
- title: "Unconfigured LLM templates"
- description: "LLMs (Large Language Models) are AI tools optimized for tasks like summarizing content, generating reports, automating customer interactions, and facilitating forum moderation and insights"
- fake: "Manual configuration"
- button: "Set up"
- next:
- title: "Next"
-
- tests:
- title: "Run test"
- running: "Running test..."
- success: "Success!"
- failure: "Trying to contact the model returned this error: %{error}"
-
- hints:
- max_prompt_tokens: "The maximum number of tokens the model can process in a single request"
- max_output_tokens: "The maximum number of tokens the model can generate in a single request"
- display_name: "The name used to reference this model across your site's interface."
- name: "We include this in the API call to specify which model we'll use"
- vision_enabled: "If enabled, the AI will attempt to understand images. It depends on the model being used supporting vision. Supported by latest models from Anthropic, Google, and OpenAI."
- enabled_chat_bot: "If enabled, users can select this model when creating PMs with the AI bot"
- cost_input: "The input cost per 1M tokens for this model"
- cost_cached_input: "The cached input cost per 1M tokens for this model"
- cost_output: "The output cost per 1M tokens for this model"
- cost_measure: "$/1M tokens"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Custom"
-
- provider_fields:
- access_key_id: "AWS Bedrock access key ID"
- region: "AWS Bedrock region"
- organization: "Optional OpenAI organization ID"
- disable_system_prompt: "Disable system message in prompts"
- enable_native_tool: "Enable native tool support"
- disable_native_tools: "Disable native tool support (use XML based tools)"
- provider_order: "Provider order (comma delimited list)"
- provider_quantizations: "Order of provider quantizations (comma delimited list eg: fp16,fp8)"
- disable_streaming: "Disable streaming completions (convert streaming to non streaming requests)"
- reasoning_effort: "Reasoning effort (only applicable to reasoning models)"
- enable_reasoning: "Enable reasoning (only applicable to reasoning models)"
- enable_thinking: "Enable thinking (only on applicable models eg: flash 2.5)"
- thinking_tokens: "Number of tokens used for thinking"
- reasoning_tokens: "Number of tokens used for reasoning"
- disable_temperature: "Disable temperature (some thinking models don't support temperature)"
- disable_top_p: "Disable top P (some thinking models don't support top P)"
- enable_responses_api: "Enable responses API (required on certain OpenAI models)"
-
- related_topics:
- title: "Related topics"
- pill: "Related"
- ai_helper:
- title: "Suggest changes using AI"
- description: "Choose one of the options below, and the AI will suggest you a new version of the text."
- selection_hint: "Hint: You can also select a portion of the text before opening the helper to rewrite only that."
- suggest: "Suggest with AI"
- suggest_errors:
- too_many_tags:
- one: "You can only have up to %{count} tag"
- other: "You can only have up to %{count} tags"
- no_suggestions: "No suggestions available"
- missing_content: "Please enter some content to generate suggestions."
- context_menu:
- trigger: "Ask AI"
- loading: "AI is generating"
- cancel: "Cancel"
- regen: "Try again"
- confirm: "Confirm"
- discard: "Discard"
- changes: "Suggested edits"
- custom_prompt:
- title: "Custom prompt"
- placeholder: "Enter a custom prompt..."
- submit: "Send prompt"
- translate_prompt: "Translate to %{language}"
- post_options_menu:
- trigger: "Ask AI"
- title: "Ask AI"
- loading: "AI is generating"
- close: "Close"
- copy: "Copy"
- copied: "Copied!"
- cancel: "Cancel"
- insert_footnote: "Add footnote"
- footnote_disabled: "Automatic insertion disabled, click copy button and edit it in manually"
- footnote_credits: "Explanation by AI"
- fast_edit:
- suggest_button: "Suggest edit"
- thumbnail_suggestions:
- title: "Suggested thumbnails"
- select: "Select"
- selected: "Selected"
- image_caption:
- button_label: "Caption with AI"
- generating: "Generating caption..."
- credits: "Captioned by AI"
- save_caption: "Save"
- automatic_caption_setting: "Enable auto caption"
- automatic_caption_loading: "Captioning images..."
- automatic_caption_dialog:
- prompt: "This post contains non-captioned images. Would you like to enable automatic captions on image uploads? (This can be changed in your preferences later)"
- confirm: "Enable"
- cancel: "Don't ask again"
- no_content_error: "Add content first to perform AI actions on it"
-
- reviewables:
- model_used: "Model used:"
- accuracy: "Accuracy:"
-
- embeddings:
- short_title: "Embeddings"
- description: "Embeddings are numerical representations of data that capture meaning and relationships, enabling Discourse AI features like Related Topics and AI Search to understand and connect content."
- new: "New embedding"
- back: "Back"
- save: "Save"
- saved: "Embedding configuration saved"
- delete: "Delete"
- confirm_delete: Are you sure you want to remove this embedding configuration?
- empty: "You haven't setup embeddings yet"
- presets: "Select a preset..."
- configure_manually: "Configure manually"
- edit: "Edit"
- seeded_warning: "This is pre-configured on your site and cannot be edited."
- tests:
- title: "Run test"
- running: "Running test..."
- success: "Success!"
- failure: "Attempting to generate an embedding resulted in: %{error}"
- hints:
- dimensions_warning: "Once saved, this value can't be changed."
- matryoshka_dimensions: "Defines the size of nested embeddings used for hierarchical or multi-layered representation of data, similar to how nested dolls fit within each other."
- embed_prompt: "Task instruction prefix when generating embeddings of forum content. ONLY for embeddings models that require prefixes, like nomic-embed or stella. Not needed for most models."
- search_prompt: "Task instruction prefix when generating embeddings of search queries. ONLY for embeddings models that require prefixes, like nomic-embed or stella. Not needed for most models."
- sequence_length: "The maximum number of tokens that can be processed at once when creating embeddings or handling a query."
- distance_function: "Determines how similarity between embeddings is calculated, using either cosine distance (measuring the angle between vectors) or negative inner product (measuring overlap of vector values)."
- display_name: "Name"
- provider: "Provider"
- url: "Embeddings service URL"
- api_key: "Embeddings service API Key"
- tokenizer: "Tokenizer"
- dimensions: "Embedding dimensions"
- max_sequence_length: "Sequence length"
- embed_prompt: "Embed prompt"
- search_prompt: "Search prompt"
- matryoshka_dimensions: "Matryoshka dimensions"
-
- distance_function: "Distance function"
- distance_functions:
- <#>: "Negative inner product"
- <=>: "Cosine distance"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Custom"
- provider_fields:
- model_name: "Model name"
-
- semantic_search: "Topics (Semantic)"
- semantic_search_loading: "Searching for more results using AI"
- semantic_search_results:
- toggle: "Showing %{count} results found using AI"
- toggle_hidden: "Hiding %{count} results found using AI"
- none: "Sorry, our AI search found no matching topics"
- new: "Press 'search' to begin looking for new results with AI"
- unavailable: "AI results unavailable"
- semantic_search_tooltips:
- results_explanation: "When enabled, additional AI search results will be added below."
- invalid_sort: "Search results must be sorted by Relevance to display AI results"
- semantic_search_unavailable_tooltip: "Search results must be sorted by Relevance to display AI results"
- ai_generated_result: "Search result found using AI"
- quick_search:
- suffix: "in all topics and posts with AI"
-
- ai_artifact:
- expand_view_label: "Expand view"
- collapse_view_label: "Exit Fullscreen (ESC or Back button)"
- click_to_run_label: "Run Artifact"
-
- ai_bot:
- persona: "Persona"
- llm: "Model"
- pm_warning: "AI chatbot messages are monitored regularly by moderators."
- cancel_streaming: "Stop reply"
- default_pm_prefix: "[Untitled AI bot PM]"
- shortcut_title: "Start a PM with an AI bot"
- share: "Copy AI conversation"
- conversation_shared: "Conversation copied"
- embed_copied: "Embed copied to clipboard"
- debug_ai: "View raw AI request and response"
- sidebar_empty: "Bot conversation history will appear here."
- debug_ai_modal:
- title: "View AI interaction"
- copy_request: "Copy request"
- copy_response: "Copy response"
- request_tokens: "Request tokens:"
- response_tokens: "Response tokens:"
- request: "Request"
- response: "Response"
- next_log: "Next"
- previous_log: "Previous"
-
- share_full_topic_modal:
- title: "Share conversation publicly"
- share: "Share and copy link"
- update: "Update and copy link"
- delete: "Delete share"
-
- share_ai_conversation:
- name: "Share AI conversation"
- title: "Share this AI conversation publicly"
-
- invite_ai_conversation:
- button: "Invite"
- title: "Invite to AI conversation"
-
- ai_label: "AI"
- ai_title: "Conversation with AI"
-
- share_modal:
- title: "Copy AI conversation"
- copy: "Copy"
- context: "Interactions to share:"
- share_tip: "Alternatively, you can share the entire conversation"
-
- bot_names:
- fake: "Fake Test Bot"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- header: "What can I help with?"
- submit: "Submit question"
- disclaimer: "Generative AI can make mistakes. Verify important information."
- placeholder: "Ask a question..."
- new: "New Question"
- min_input_length_message:
- one: "Message must be 1 character or longer"
- other: "Message must be %{count} characters or longer"
- messages_sidebar_title: "Conversations"
- today: "Today"
- last_7_days: "Last 7 days"
- last_30_days: "Last 30 days"
- upload_files: "Upload files"
- uploads_in_progress: "Cannot submit while uploads are in progress"
- sentiments:
- dashboard:
- title: "Sentiment"
- sidebar:
- overview: "Sentiment overview"
- analysis: "Sentiment analysis"
- sentiment_analysis:
- share_chart: "Copy link to chart"
- filter_types:
- all: "All"
- positive: "Positive"
- neutral: "Neutral"
- negative: "Negative"
- group_types:
- category: "Category"
- tag: "Tag"
- table:
- sentiment: "Sentiment"
- total_count: "Total"
-
- summarization:
- chat:
- title: "Summarize messages"
- description: "Select an option below to summarize the conversation sent during the desired timeframe."
- summarize: "Summarize"
- since:
- one: "Last hour"
- other: "Last %{count} hours"
- topic:
- title: "Topic summary"
- close: "Close summary panel"
- topic_list_layout:
- button:
- compact: "Compact"
- expanded: "Expanded"
- expanded_description: "with AI summaries"
-
- discobot_discoveries:
- main_title: "Discobot discoveries"
- regular_results: "Topics"
- tell_me_more: "Tell me more..."
- continue_convo: "Continue conversation..."
- loading_convo: "Loading conversation"
- collapse: "Collapse"
- timed_out: "Discobot couldn't find any discoveries. Something went wrong."
- user_setting: "Enable search discoveries"
- tooltip:
- header: "AI powered search"
- content: "Natural language search powered by %{model}"
- actions:
- info: "How does it work?"
- disable: "Disable"
-
- user_preferences:
- empty: "There are no relevant settings available at this time"
- review:
- types:
- reviewable_ai_post:
- title: "AI-Flagged post"
- reviewable_ai_chat_message:
- title: "AI-Flagged chat message"
diff --git a/config/locales/client.en_GB.yml b/config/locales/client.en_GB.yml
deleted file mode 100644
index c2a0d3d6..00000000
--- a/config/locales/client.en_GB.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-en_GB:
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- categories:
- label: "Categories"
- discourse_ai:
- usage:
- summary: "Summary"
- ai_persona:
- description: "Description"
- tools:
- description: "Description"
- summary: "Summary"
diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml
deleted file mode 100644
index 2836dec0..00000000
--- a/config/locales/client.es.yml
+++ /dev/null
@@ -1,686 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-es:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Permite la búsqueda de IA"
- stream_completion: "Permite la transmisión de realización de personas de IA"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "Emoción"
- description: "La tabla muestra un recuento de los mensajes clasificados con una emoción determinada. Clasificados con el modelo 'SamLowe/roberta-base-go_emotions'."
- reports:
- filters:
- sort_by:
- label: "Ordenar por"
- tag:
- label: "Etiquetar"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Remitente"
- description: "El usuario que enviará el informe"
- receivers:
- label: "Receptores"
- description: "Los usuarios que recibirán el informe (los correos electrónicos se enviarán directamente, los nombres de usuario se enviarán por MP)"
- topic_id:
- label: "ID del tema"
- description: "El ID del tema donde se publicará el informe"
- title:
- label: "Título"
- description: "El título del informe"
- days:
- label: "Días"
- description: "El plazo del informe"
- offset:
- label: "Compensación"
- description: "Cuando realices pruebas, es posible que quieras ejecutar el informe históricamente, utiliza la compensación para iniciar el informe en una fecha anterior"
- instructions:
- label: "Instrucciones"
- description: "Las instrucciones proporcionadas al modelo de lenguaje grande"
- sample_size:
- label: "Tamaño de la muestra"
- description: "El número de publicaciones que se van a muestrear para el informe"
- tokens_per_post:
- label: "Tokens por publicación"
- description: "La cantidad de tokens LLM que se usarán por publicación"
- model:
- label: "Modelo"
- description: "LLM a utilizar para la generación de informes"
- categories:
- label: "Categorías"
- description: "Filtrar temas solo para esta categoría"
- tags:
- label: "Etiquetas"
- description: "Filtrar temas solo para estas etiquetas"
- exclude_tags:
- label: "Excluir etiquetas"
- description: "Excluir temas con estas etiquetas"
- exclude_categories:
- label: "Excluir categorías"
- description: "Excluir temas con estas categorías"
- allow_secure_categories:
- label: "Permitir categorías seguras"
- description: "Permitir que se genere el informe para temas en categorías seguras"
- suppress_notifications:
- label: "Suprimir notificaciones"
- description: "Suprime las notificaciones que pueda generar el informe transformando a contenido. Esto reasignará las menciones y los enlaces internos."
- debug_mode:
- label: "Modo de depuración"
- description: "Activa el modo de depuración para ver la entrada y salida brutas del LLM"
- priority_group:
- label: "Grupo prioritario"
- description: "Priorizar el contenido de este grupo en el informe"
- temperature:
- label: "Temperatura"
- top_p:
- label: "Top P"
- llm_tool_triage:
- fields:
- model:
- label: "Modelo"
- llm_triage:
- fields:
- system_prompt:
- label: "Aviso del sistema"
- description: "El aviso que se utilizará para el triaje, asegúrate de que responda con una sola palabra que puedas utilizar para desencadenar la acción"
- max_post_tokens:
- label: "Máximo de tokens de publicación"
- description: "El número máximo de tokens a escanear utilizando el triaje LLM"
- stop_sequences:
- label: "Detener secuencias"
- description: "Ordena al modelo que detenga la generación de tokens cuando llegue a uno de estos valores"
- search_for_text:
- label: "Buscar texto"
- description: "Si aparece el siguiente texto en la respuesta LLM, aplicar estas acciones"
- category:
- label: "Categoría"
- description: "Categoría para aplicar al tema"
- tags:
- label: "Etiquetas"
- description: "Etiquetas para aplicar al tema"
- canned_reply:
- label: "Responder"
- description: "Texto sin formato de respuesta enlatada para publicar sobre el tema"
- canned_reply_user:
- label: "Responder al usuario"
- description: "Nombre de usuario del usuario para publicar la respuesta enlatada"
- hide_topic:
- label: "Ocultar tema"
- description: "Hacer que el tema no sea visible para el público si se activa"
- flag_type:
- label: "Tipo de denuncia"
- description: "Tipo de denuncia que se aplicará a la publicación (spam o simplemente remitir para su revisión)"
- flag_post:
- label: "Denunciar publicación"
- description: "Denuncia la publicación (como spam o para revisión)"
- include_personal_messages:
- label: "Incluir mensajes personales"
- description: "También escanea y clasifica los mensajes personales"
- model:
- label: "Modelo"
- description: "Modelo lingüístico utilizado para el triaje"
- temperature:
- label: "Temperatura"
- discourse_ai:
- title: "IA"
- features:
- back: "Volver"
- disabled: "(desactivado)"
- groups: "Grupos:"
- no_persona: "No establecido"
- no_groups: "Ninguno"
- edit: "Editar"
- expand_list:
- one: "(%{count} más)"
- other: "(%{count} más)"
- collapse_list: "(mostrar menos)"
- filters:
- all: "Todos"
- reset: "Restablecer"
- search:
- name: "Buscar"
- embeddings:
- name: "Incrustaciones"
- ai_helper:
- name: "Ayudante"
- proofread: Corregir el texto
- explain: "Explicar"
- smart_dates: "Fechas inteligentes"
- markdown_tables: "Generar tabla Markdown"
- custom_prompt: "Instruccón personalizada"
- spam:
- name: "Spam"
- description: "Identifica el correo no deseado potencial utilizando el LLM seleccionado y lo marca para que los moderadores del sitio lo inspeccionen en la cola de revisión"
- modals:
- select_option: "Selecciona una opción..."
- spam:
- short_title: "Spam"
- title: "Configurar el manejo de correo no deseado"
- select_llm: "Seleccionar LLM"
- custom_instructions: "Instrucciones personalizadas"
- custom_instructions_help: "Instrucciones personalizadas específicas de tu sitio para ayudar a guiar a la IA en la identificación del correo no deseado, por ejemplo: «Sé más agresivo a la hora de escanear los mensajes que no estén en inglés»."
- last_seven_days: "Últimos 7 días"
- scanned_count: "Publicaciones escaneadas"
- false_positives: "Denunciado incorrectamente"
- false_negatives: "Correo no deseado perdido"
- spam_detected: "Correo no deseado detectado"
- custom_instructions_placeholder: "Instrucciones específicas del sitio para que la IA ayude a identificar el correo no deseado con mayor precisión"
- enable: "Activar"
- spam_tip: "La detección de correo no deseado mediante IA escaneará los 3 primeros mensajes de todos los usuarios nuevos en temas públicos. Los marcará para su revisión y bloqueará a los usuarios si es probable que sean correo no deseado."
- settings_saved: "Ajustes guardados"
- spam_description: "Identifica el correo no deseado potencial utilizando el LLM seleccionado y lo marca para que los moderadores del sitio lo inspeccionen en la cola de revisión"
- no_llms: "No hay LLM disponibles"
- test_button: "Prueba..."
- save_button: "Guardar cambios"
- test_modal:
- title: "Probar la detección de correo no deseado"
- post_url_label: "URL o ID de la publicación"
- post_url_placeholder: "https://tu-foro.com/t/topic/123/4 o ID de publicación"
- result: "Resultado"
- scan_log: "Registro de escaneo"
- run: "Realizar prueba"
- spam: "Spam"
- not_spam: "No es correo no deseado"
- stat_tooltips:
- incorrectly_flagged: "Elementos que el bot de IA marcó como correo no deseado en los que los moderadores no estaban de acuerdo"
- missed_spam: "Elementos denunciados por la comunidad como correo no deseado que no fueron detectados por el bot de IA, con los que los moderadores estaban de acuerdo"
- errors:
- scan_not_admin:
- message: "Advertencia: el escaneo de correo no deseado no funcionará correctamente porque la cuenta de escaneo de correo no deseado no es un administrador"
- action: "Corregir"
- resolved: "¡Se ha resuelto el error!"
- usage:
- short_title: "Uso"
- summary: "Resumen"
- total_tokens: "Tokens totales"
- tokens_over_time: "Tokens a lo largo del tiempo"
- features_breakdown: "Uso por característica"
- feature: "Característica"
- usage_count: "Recuento de usos"
- model: "Modelo"
- models_breakdown: "Uso por modelo"
- users_breakdown: "Uso por usuario"
- all_features: "Todas las características"
- all_models: "Todos los modelos"
- username: "Nombre de usuario"
- total_requests: "Peticiones totales"
- request_tokens: "Tokens de solicitud"
- response_tokens: "Tokens de respuesta"
- net_request_tokens: "Tokens de solicitud de red"
- cached_tokens: "Tokens almacenados en caché"
- cached_request_tokens: "Tokens de solicitud almacenados en caché"
- no_users: "No se han encontrado datos de uso del usuario"
- no_models: "No se han encontrado datos de uso del modelo"
- no_features: "No se han encontrado datos de uso de características"
- subheader_description: "Los tokens son las unidades básicas que utilizan los LLM para comprender y generar texto, los datos de uso pueden afectar a los costes"
- stat_tooltips:
- total_requests: "Todas las solicitudes realizadas a los LLM a través de Discourse"
- total_tokens: "Todos los tokens utilizados al solicitar un LLM"
- request_tokens: "Tokens utilizados cuando el LLM intenta comprender lo que dices"
- response_tokens: "Tokens utilizados cuando el LLM responde a tu consulta"
- cached_tokens: "Tokens de solicitud previamente procesados que el LLM reutiliza para optimizar el rendimiento y el coste"
- periods:
- last_day: "Últimas 24 horas"
- last_week: "Última semana"
- last_month: "Último mes"
- custom: "Personalizado..."
- ai_persona:
- ai_tools: "Herramientas"
- tool_strategies:
- all: "Aplicar a todas las respuestas"
- replies:
- one: "Aplicar solo a la primera respuesta"
- other: "Aplicar a las primeras %{count}} respuestas"
- back: "Atrás"
- name: "Nombre"
- edit: "Editar"
- export: "Exportar"
- description: "Descripción"
- no_llm_selected: "No se seleccionó ningún modelo de idioma"
- max_context_posts: "Número máximo de publicaciones contextuales"
- max_context_posts_help: "El número máximo de mensajes a utilizar como contexto para la IA cuando responda a un usuario. (vacío por defecto)"
- vision_enabled: Visión activada
- vision_enabled_help: Si está activada, la IA intentará comprender las imágenes que los usuarios publiquen en el tema, depende del modelo que se utilice para soportar la visión. Compatible con los últimos modelos de Anthropic, Google y OpenAI.
- vision_max_pixels: Tamaño de imagen admitido
- vision_max_pixel_sizes:
- low: 'Baja calidad: más barato (256x256)'
- medium: Calidad media (512x512)
- high: 'Alta calidad: más lento (1024x1024)'
- tool_details: Mostrar detalles de la herramienta
- tool_details_help: Mostrará a los usuarios finales detalles sobre qué herramientas ha activado el modelo de lenguaje.
- mentionable: Permitir menciones
- mentionable_help: Si está activada, los usuarios de los grupos permitidos pueden mencionar a este usuario en sus mensajes, y la IA responderá como esta persona.
- user: Usuario
- create_user: Crear usuario
- create_user_help: Opcionalmente, puedes adjuntar un usuario a esta persona. Si lo haces, la IA utilizará a este usuario para responder a las solicitudes.
- default_llm: Modelo lingüístico por defecto
- default_llm_help: El modelo de idioma por defecto que se utilizará para esta persona. Obligatorio si deseas mencionar a la persona en publicaciones públicas.
- question_consolidator_llm: Modelo lingüístico para el consolidador de preguntas
- question_consolidator_llm_help: El modelo lingüístico a utilizar para el consolidador de preguntas, puedes elegir un modelo menos potente para ahorrar costes.
- system_prompt: Aviso del sistema
- forced_tool_strategy: Estrategia de herramienta forzada
- allow_chat_direct_messages: "Permitir mensajes directos de chat"
- allow_chat_direct_messages_help: "Si se activa, los usuarios de los grupos permitidos pueden enviar mensajes directos a esta persona."
- allow_chat_channel_mentions: "Permitir menciones en el canal de chat"
- allow_chat_channel_mentions_help: "Si se activa, los usuarios de los grupos permitidos pueden mencionar a esta persona en los canales de chat."
- allow_personal_messages: "Permitir mensajes personales"
- allow_personal_messages_help: "Si se activa, los usuarios de los grupos permitidos pueden enviar mensajes personales a esta persona."
- allow_topic_mentions: "Permitir menciones en temas"
- allow_topic_mentions_help: "Si se activa, los usuarios de los grupos permitidos pueden mencionar a esta persona en los temas."
- force_default_llm: "Utilizar siempre el modelo de idioma por defecto"
- save: "Guardar"
- saved: "Persona guardada"
- enabled: "¿Activado?"
- tools: "Herramientas habilitadas"
- forced_tools: "Herramientas forzadas"
- allowed_groups: "Grupos permitidos"
- confirm_delete: "¿Seguro que quieres eliminar esta persona?"
- new: "Nueva persona"
- no_personas: "Aún no has creado ninguna persona"
- title: "Personas"
- short_title: "Personas"
- delete: "Eliminar"
- temperature: "Temperatura"
- temperature_help: "Temperatura que se utilizará para el LLM. Aumentar para aumentar la creatividad (dejar vacío para utilizar el valor por defecto del modelo, generalmente un valor de 0,0 a 2,0)"
- top_p: "Top P"
- top_p_help: "Top P a utilizar para el LLM, aumentar para aumentar la aleatoriedad (dejar vacío para utilizar el modelo por defecto, generalmente un valor de 0,0 a 1,0)"
- priority: "Prioridad"
- priority_help: "Las personas prioritarias se muestran a los usuarios en la parte superior de la lista de personas. Si varias personas tienen prioridad, se ordenarán alfabéticamente."
- tool_options: "Opciones de herramientas"
- rag_conversation_chunks: "Buscar fragmentos de conversación"
- rag_conversation_chunks_help: "El número de fragmentos a utilizar para las búsquedas del modelo RAG. Aumentar para incrementar la cantidad de contexto que puede utilizar la IA."
- persona_description: "Las personas son una potente característica que te permite personalizar el comportamiento del motor de IA en tu foro de Discourse. Actúan como un «mensaje del sistema» que guía las respuestas e interacciones de la IA, ayudando a crear una experiencia de usuario más personalizada y atractiva."
- response_format:
- open_modal: "Editar"
- modal:
- key_title: "Clave"
- filters:
- reset: "Restablecer"
- rag:
- options:
- rag_chunk_tokens: "Cargar tokens de fragmentos"
- rag_chunk_tokens_help: "El número de tokens a utilizar para cada fragmento en el modelo RAG. Aumentar para incrementar la cantidad de contexto que puede utilizar la IA. (Al cambiar se reindexarán todas las cargas)"
- rag_chunk_overlap_tokens: "Cargar tokens de solapamiento de fragmentos"
- rag_chunk_overlap_tokens_help: "El número de tokens a solapar entre fragmentos en el modelo RAG. (al cambiar se reindexarán todas las cargas)"
- show_indexing_options: "Mostrar opciones de carga"
- hide_indexing_options: "Ocultar opciones de carga"
- uploads:
- title: "Cargas"
- button: "Añadir archivos"
- filter: "Filtrar cargas"
- indexed: "Indexado"
- indexing: "Indexando"
- uploaded: "Listo para ser indexado"
- uploading: "Subiendo..."
- remove: "Eliminar carga"
- tools:
- back: "Atrás"
- short_title: "Herramientas"
- export: "Exportar"
- no_tools: "Aún no has creado ninguna herramienta"
- name: "Nombre"
- new: "Nueva herramienta"
- description: "Descripción"
- description_help: "Una descripción clara de la finalidad de la herramienta para el modelo lingüístico"
- subheader_description: "Las herramientas amplían las capacidades de los robots de IA con funciones JavaScript definidas por el usuario."
- summary: "Resumen"
- summary_help: "Resumen de la finalidad de las herramientas que se mostrará a los usuarios finales"
- script: "Script"
- parameters: "Parámetros"
- save: "Guardar"
- remove_parameter: "Eliminar"
- parameter_required: "Obligatorio"
- parameter_enum: "Enumeración"
- parameter_name: "Nombre del parámetro"
- parameter_description: "Descripción del parámetro"
- enum_value: "Valor de enumeración"
- add_enum_value: "Añadir valor de enumeración"
- edit: "Editar"
- test: "Realizar prueba"
- delete: "Eliminar"
- saved: "Herramienta guardada"
- confirm_delete: "¿Seguro que quieres eliminar esta herramienta?"
- test_modal:
- title: "Probar la herramienta de IA"
- run: "Realizar prueba"
- result: "Resultado de la prueba"
- llms:
- short_title: "LLM"
- no_llms: "Aún no hay LLM"
- new: "Nuevo modelo"
- display_name: "Nombre"
- name: "ID del modelo"
- provider: "Proveedor"
- tokenizer: "Tokenizador"
- url: "URL del servicio que aloja el modelo"
- api_key: "Clave API del servicio que aloja el modelo"
- enabled_chat_bot: "Permitir selector de bot de IA"
- vision_enabled: "Visión activada"
- ai_bot_user: "Usuario de bot de IA"
- save: "Guardar"
- edit: "Editar"
- saved: "Modelo LLM guardado"
- back: "Atrás"
- confirm_delete: '¿Seguro que quieres eliminar este modelo?'
- delete: Eliminar
- seeded_warning: "Este modelo está preconfigurado en tu web y no se puede editar."
- quotas:
- title: "Cuotas de uso"
- add_title: "Crear nueva cuota"
- group: "Grupo"
- max_tokens: "Máximo de tokens"
- max_usages: "Máximo de usos"
- duration: "Duración"
- confirm_delete: "¿Seguro que quieres eliminar esta cuota?"
- add: "Añadir cuota"
- durations:
- hour: "1 hora"
- six_hours: "6 horas"
- day: "24 horas"
- week: "7 días"
- custom: "Personalizado..."
- hours: "horas"
- max_tokens_help: "Número máximo de tokens (palabras y caracteres) que cada usuario de este grupo puede utilizar dentro de la duración especificada. Los tokens son las unidades que utilizan los modelos de IA para procesar texto: aproximadamente 1 token = 4 caracteres o 3/4 de una palabra."
- max_usages_help: "Número máximo de veces que cada usuario de este grupo puede utilizar el modelo de IA dentro de la duración especificada. Esta cuota se controla por usuario individual, no se comparte con todo el grupo."
- usage:
- ai_bot: "Bot de IA"
- ai_helper: "Ayudante"
- ai_persona: "Persona (%{persona})"
- ai_summarization: "Resumir"
- ai_embeddings_semantic_search: "Búsqueda de IA"
- ai_spam: "Spam"
- in_use_warning:
- one: "Este modelo lo utiliza actualmente %{settings}. Si está mal configurado, la característica no funcionará como se espera."
- other: "Este modelo lo utilizan actualmente las siguientes %{settings}. Si está mal configurado, las características no funcionarán como se espera."
- model_description:
- none: "Ajustes generales que funcionan para la mayoría de los modelos lingüísticos"
- anthropic-claude-opus-4-0: "El modelo antrópico más inteligente"
- anthropic-claude-3-5-haiku-latest: "Rápido y rentable"
- google-gemini-2-5-flash: "Ligero, rápido y rentable con razonamiento multimodal"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Modelo multilingüe, ligero y eficaz"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Potente modelo polivalente"
- mistral-mistral-large-latest: "El modelo más potente de Mistral"
- mistral-pixtral-large-latest: "El modelo más potente de Mistral con capacidad de visión"
- preseeded_model_description: "Modelo de código abierto preconfigurado que utiliza %{model}"
- configured:
- title: "LLM configurados"
- preconfigured_llms: "Selecciona tu LLM"
- preconfigured:
- title_no_llms: "Selecciona una plantilla para empezar"
- title: "Plantillas LLM no configuradas"
- description: "Los LLM (Large Language Models) son herramientas de IA optimizadas para tareas como resumir contenidos, generar informes, automatizar las interacciones con los clientes y facilitar la moderación y los comentarios en los foros"
- fake: "Configuración manual"
- button: "Configurar"
- next:
- title: "Siguiente"
- tests:
- title: "Realizar prueba"
- running: "Ejecutando prueba..."
- success: "Éxito!"
- failure: "Al intentar contactar con la modelo, se devolvió este error: %{error}"
- hints:
- name: "Incluimos esto en la llamada a la API para especificar qué modelo utilizaremos"
- vision_enabled: "Si está activada, la IA intentará comprender las imágenes. Depende del modelo utilizado que soporte la visión. Compatible con los últimos modelos de Anthropic, Google y OpenAI."
- enabled_chat_bot: "Si se activa, los usuarios pueden seleccionar este modelo al crear MPs con el bot de IA"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Personalizado"
- provider_fields:
- access_key_id: "ID de la clave de acceso a AWS Bedrock"
- region: "Región de AWS Bedrock"
- organization: "ID de organización de OpenAI opcional"
- disable_system_prompt: "Desactivar el mensaje del sistema en las instrucciones"
- enable_native_tool: "Activar la compatibilidad con herramientas nativas"
- disable_native_tools: "Desactivar el soporte de herramientas nativas (usar herramientas basadas en XML)"
- provider_order: "Orden de proveedores (lista delimitada por comas)"
- provider_quantizations: "Orden de las cuantificaciones de los proveedores (lista delimitada por comas, por ejemplo: fp16, fp8)"
- disable_streaming: "Desactiva las finalizaciones de streaming (convierte las peticiones de streaming en no streaming)"
- related_topics:
- title: "Temas relacionados"
- pill: "Relacionados"
- ai_helper:
- title: "Sugerir cambios usando IA"
- description: "Elige una de las opciones siguientes y la IA te propondrá una nueva versión del texto."
- selection_hint: "Sugerencia: También puedes seleccionar una parte del texto antes de abrir el asistente para reescribir solo eso."
- suggest: "Sugerir con IA"
- suggest_errors:
- too_many_tags:
- one: "Solo puedes tener hasta %{count} etiqueta"
- other: "Solo puedes tener hasta %{count} etiquetas"
- no_suggestions: "No hay sugerencias disponibles"
- missing_content: "Introduce algún contenido para generar sugerencias."
- context_menu:
- trigger: "Preguntar a la IA"
- loading: "La IA está generando"
- cancel: "Cancelar"
- confirm: "Confirmar"
- discard: "Descartar"
- changes: "Ediciones sugeridas"
- custom_prompt:
- title: "Instruccón personalizada"
- placeholder: "Introduzca un aviso personalizado..."
- submit: "Enviar instrucción"
- translate_prompt: "Traducir a %{language}"
- post_options_menu:
- trigger: "Pregúntale a la IA"
- title: "Pregúntale a la IA"
- loading: "La IA está generando"
- close: "Cerrar"
- copy: "Copiar"
- copied: "¡Copiado!"
- cancel: "Cancelar"
- insert_footnote: "Añadir nota al pie de página"
- footnote_disabled: "Inserción automática desactivada, pulsa el botón copiar y edítalo manualmente"
- footnote_credits: "Explicación por IA"
- fast_edit:
- suggest_button: "Sugerir edición"
- thumbnail_suggestions:
- title: "Miniaturas sugeridas"
- select: "Seleccionar"
- selected: "Seleccionado"
- image_caption:
- button_label: "Subtítulo con IA"
- generating: "Generando subtítulo..."
- credits: "Subtitulado por IA"
- save_caption: "Guardar"
- automatic_caption_setting: "Activar subtítulos automáticos"
- automatic_caption_loading: "Subtitulando imágenes..."
- automatic_caption_dialog:
- prompt: "Esta publicación contiene imágenes sin subtítulos. ¿Te gustaría activar los subtítulos automáticos en las imágenes subidas? (Puedes cambiarlo en tus preferencias más adelante)"
- confirm: "Activar"
- cancel: "No volver a preguntar"
- no_content_error: "Añade contenido primero para realizar acciones de IA sobre él"
- reviewables:
- model_used: "Modelo utilizado:"
- accuracy: "Precisión:"
- embeddings:
- short_title: "Incrustaciones"
- new: "Nueva incrustación"
- back: "Atrás"
- save: "Guardar"
- saved: "Configuración de incrustación guardada"
- delete: "Eliminar"
- confirm_delete: '¿Seguro que quieres eliminar esta configuración de incrustación?'
- empty: "Aún no has configurado incrustaciones"
- presets: "Selecciona un preajuste..."
- configure_manually: "Configurar manualmente"
- edit: "Editar"
- seeded_warning: "Esto está preconfigurado en tu web y no se puede editar."
- tests:
- title: "Realizar prueba"
- running: "Ejecutando prueba..."
- success: "¡Éxito!"
- failure: "El intento de generar una incrustación dio como resultado: %{error}"
- hints:
- dimensions_warning: "Una vez guardado, este valor no se puede cambiar."
- matryoshka_dimensions: "Define el tamaño de las incrustaciones anidadas utilizadas para la representación jerárquica o en varias capas de los datos, de forma similar a como encajan los muñecos anidados unos dentro de otros."
- sequence_length: "El número máximo de tokens que se pueden procesar a la vez al crear incrustaciones o gestionar una consulta."
- distance_function: "Determina cómo se calcula la similitud entre las incrustaciones, utilizando la distancia del coseno (que mide el ángulo entre los vectores) o el producto interior negativo (que mide el solapamiento de los valores de los vectores)."
- display_name: "Nombre"
- provider: "Proveedor"
- url: "URL del servicio de incrustación"
- api_key: "Clave API del servicio de incrustación"
- tokenizer: "Tokenizador"
- dimensions: "Dimensiones de incrustación"
- max_sequence_length: "Longitud de la secuencia"
- embed_prompt: "Incrustar aviso"
- search_prompt: "Buscar aviso"
- matryoshka_dimensions: "Dimensiones de la matrioska"
- distance_function: "Función de distancia"
- distance_functions:
- "<#>": "Producto interno negativo"
- <=>: "Distancia del coseno"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Personalizado"
- provider_fields:
- model_name: "Nombre del modelo"
- semantic_search: "Temas (semánticos)"
- semantic_search_loading: "Buscando más resultados usando IA"
- semantic_search_results:
- toggle: "Mostrando %{count} resultados encontrados usando IA"
- toggle_hidden: "Ocultando %{count} resultados encontrados usando IA"
- none: "Lo sentimos, nuestra búsqueda mediante IA no ha encontrado temas coincidentes"
- new: "Pulsa «Buscar» para empezar a buscar nuevos resultados con IA"
- unavailable: "Resultados de IA no disponibles"
- semantic_search_tooltips:
- results_explanation: "Cuando esté activada, se añadirán resultados de búsqueda de IA adicionales a continuación."
- invalid_sort: "Los resultados de la búsqueda deben ordenarse por Relevancia para mostrar resultados de IA"
- semantic_search_unavailable_tooltip: "Los resultados de la búsqueda deben ordenarse por Relevancia para mostrar resultados de IA"
- ai_generated_result: "Resultado de búsqueda encontrado mediante IA"
- quick_search:
- suffix: "en todos los temas y publicaciones con IA"
- ai_artifact:
- expand_view_label: "Ampliar vista"
- collapse_view_label: "Salir de la pantalla completa (ESC o botón Atrás)"
- click_to_run_label: "Ejecutar Artefacto"
- ai_bot:
- llm: "Modelo"
- pm_warning: "Los moderadores supervisan periódicamente los mensajes del chatbot de IA."
- cancel_streaming: "Detener respuesta"
- default_pm_prefix: "[MP de bot de IA sin título]"
- shortcut_title: "Iniciar un MP con un bot de IA"
- share: "Copiar conversación de IA"
- conversation_shared: "Conversación copiada"
- debug_ai: "Ver solicitud y respuesta de IA sin procesar"
- debug_ai_modal:
- title: "Ver la interacción con la IA"
- copy_request: "Copiar solicitud"
- copy_response: "Copiar respuesta"
- request_tokens: "Tokens de solicitud:"
- response_tokens: "Tokens de respuesta:"
- request: "Solicitud"
- response: "Respuesta"
- next_log: "Siguiente"
- previous_log: "Anterior"
- share_full_topic_modal:
- title: "Compartir la conversación públicamente"
- share: "Compartir y copiar enlace"
- update: "Actualizar y copiar enlace"
- delete: "Eliminar compartir"
- share_ai_conversation:
- name: "Compartir conversación de IA"
- title: "Compartir esta conversación de IA públicamente"
- invite_ai_conversation:
- button: "Invitar"
- ai_label: "IA"
- ai_title: "Conversación con IA"
- share_modal:
- title: "Copiar conversación de IA"
- copy: "Copiar"
- context: "Interacciones para compartir:"
- share_tip: "También puedes compartir toda la conversación"
- bot_names:
- fake: "Bot de prueba falso"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "Hoy"
- last_7_days: "Últimos 7 días"
- last_30_days: "Últimos 30 días"
- sentiments:
- dashboard:
- title: "Sentiment"
- sentiment_analysis:
- filter_types:
- all: "Todo"
- positive: "Positivo"
- neutral: "Neutro"
- negative: "Negativo"
- group_types:
- category: "Categoría"
- tag: "Etiquetar"
- table:
- sentiment: "Sentiment"
- total_count: "Total"
- summarization:
- chat:
- title: "Resumir mensajes"
- description: "Selecciona una opción a continuación para resumir la conversación enviada durante el periodo de tiempo deseado."
- summarize: "Resumir"
- since:
- one: "Última hora"
- other: "Últimas %{count} horas"
- topic:
- title: "Resumen del tema"
- close: "Cerrar panel de resumen"
- topic_list_layout:
- button:
- compact: "Compacto"
- expanded: "Ampliado"
- expanded_description: "con resúmenes de IA"
- discobot_discoveries:
- regular_results: "Temas"
- collapse: "Contraer"
- tooltip:
- actions:
- disable: "Desactivar"
- review:
- types:
- reviewable_ai_post:
- title: "Publicación denunciada por IA"
- reviewable_ai_chat_message:
- title: "Mensaje de chat denunciado por IA"
diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml
deleted file mode 100644
index 3bb6f7f0..00000000
--- a/config/locales/client.et.yml
+++ /dev/null
@@ -1,165 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-et:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Järjesta"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Teema ID"
- title:
- label: "Pealkiri"
- categories:
- label: "Liigid"
- tags:
- label: "Sildid"
- llm_triage:
- fields:
- category:
- label: "Foorum"
- tags:
- label: "Sildid"
- canned_reply:
- label: "Vasta"
- discourse_ai:
- features:
- back: "Tagasi"
- disabled: "(välja lülitatud)"
- groups: "Rühmad:"
- no_groups: "Pole"
- edit: "Muuda"
- expand_list:
- one: "(veel %{count})"
- other: "(veel %{count})"
- filters:
- all: "Kõik"
- reset: "Lähtesta"
- search:
- name: "Otsi"
- spam:
- name: "Spämm"
- modals:
- select_option: "Vali võimalus..."
- spam:
- short_title: "Spämm"
- enable: "Lülita sisse"
- test_modal:
- spam: "Spämm"
- usage:
- summary: "Kokkuvõte"
- username: "Kasutajanimi"
- total_requests: "Kokku päringuid"
- ai_persona:
- back: "Tagasi"
- name: "Nimi"
- edit: "Muuda"
- export: "Ekspordi"
- description: "Kirjeldus"
- user: Kasutaja
- save: "Salvesta"
- enabled: "Sisse lülitatud?"
- delete: "Kustuta"
- response_format:
- open_modal: "Muuda"
- filters:
- reset: "Lähtesta"
- rag:
- uploads:
- title: "Üleslaadimised"
- uploading: "Laen üles..."
- tools:
- back: "Tagasi"
- export: "Ekspordi"
- name: "Nimi"
- description: "Kirjeldus"
- summary: "Kokkuvõte"
- save: "Salvesta"
- remove_parameter: "Eemalda"
- parameter_required: "Nõutud"
- edit: "Muuda"
- delete: "Kustuta"
- llms:
- display_name: "Nimi"
- save: "Salvesta"
- edit: "Muuda"
- back: "Tagasi"
- delete: Kustuta
- quotas:
- group: "Grupp"
- durations:
- hour: "1 tund"
- six_hours: "6 tundi"
- day: "24 tundi"
- week: "7 päeva"
- hours: "tundi"
- usage:
- ai_spam: "Spämm"
- next:
- title: "Järgmine"
- tests:
- success: "Korras!"
- providers:
- google: "Google"
- fake: "Individuaalne"
- ai_helper:
- context_menu:
- cancel: "Tühista"
- post_options_menu:
- close: "Sulge"
- copy: "Kopeeri"
- copied: "Kopeeritud!"
- cancel: "Tühista"
- image_caption:
- save_caption: "Salvesta"
- automatic_caption_dialog:
- confirm: "Lülita sisse"
- embeddings:
- back: "Tagasi"
- save: "Salvesta"
- delete: "Kustuta"
- edit: "Muuda"
- tests:
- success: "Korras!"
- display_name: "Nimi"
- providers:
- google: "Google"
- fake: "Individuaalne"
- ai_bot:
- debug_ai_modal:
- request: "Päring"
- response: "Vastus"
- next_log: "Järgmine"
- previous_log: "Eelmine"
- invite_ai_conversation:
- button: "Kutsu"
- share_modal:
- copy: "Kopeeri"
- conversations:
- today: "Täna"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Kõik"
- neutral: "Neutraalne"
- group_types:
- category: "Foorum"
- table:
- total_count: "Kokku"
- discobot_discoveries:
- regular_results: "Teemasid"
- collapse: "Ahenda"
- tooltip:
- actions:
- disable: "Lülita välja"
diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml
deleted file mode 100644
index cc7397d9..00000000
--- a/config/locales/client.fa_IR.yml
+++ /dev/null
@@ -1,235 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-fa_IR:
- admin_js:
- admin:
- site_settings:
- categories:
- discourse_ai: "هوش مصنوعی دیسکورس"
- dashboard:
- reports:
- filters:
- sort_by:
- label: "مرتب سازی بر اساس"
- tag:
- label: "برچسب"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "فرستنده"
- topic_id:
- label: "شناسه موضوع"
- title:
- label: "عنوان"
- days:
- label: "روز"
- model:
- label: "مدل"
- categories:
- label: "دستهبندیها"
- tags:
- label: "برچسبها"
- suppress_notifications:
- label: "توقف آگاهسازیها"
- debug_mode:
- label: "حالت اشکال زدایی"
- llm_tool_triage:
- fields:
- model:
- label: "مدل"
- llm_triage:
- fields:
- search_for_text:
- label: "جستجوی متن"
- category:
- label: "دستهبندی"
- tags:
- label: "برچسبها"
- canned_reply:
- label: "پاسخ"
- canned_reply_user:
- label: "پاسخ کاربر"
- model:
- label: "مدل"
- discourse_ai:
- title: "هوش مصنوعی"
- features:
- back: "بازگشت"
- disabled: "(غیرفعال)"
- groups: "گروهها:"
- no_persona: "تنظیم نشده"
- no_groups: "هیچ کدام"
- edit: "ویرایش"
- expand_list:
- one: "(%{count} مورد دیگر)"
- other: "(%{count} مورد دیگر)"
- collapse_list: "(نمایش کمتر)"
- filters:
- all: "همه"
- reset: "بازنشانی"
- search:
- name: "جستجو"
- spam:
- name: "هرزنامه"
- modals:
- select_option: "یک گزینه را انتخاب کنید..."
- spam:
- short_title: "هرزنامه"
- last_seven_days: "۷ روز گذشته"
- enable: "فعال کردن"
- test_modal:
- spam: "هرزنامه"
- usage:
- summary: "خلاصه"
- model: "مدل"
- username: "نامکاربری"
- total_requests: "مجموع درخواستها"
- periods:
- last_day: "۲۴ ساعت گذشته"
- custom: "سفارشی..."
- ai_persona:
- back: "بازگشت"
- name: "نام"
- edit: "ویرایش"
- export: "خروجی گرفتن"
- description: "توضیح"
- user: کاربر
- save: "ذخیره"
- enabled: "فعال شده؟"
- delete: "حذف"
- response_format:
- open_modal: "ویرایش"
- modal:
- key_title: "کلید"
- filters:
- reset: "بازنشانی"
- rag:
- uploads:
- title: "بارگذاریها"
- uploading: "در حال بار گذاری..."
- tools:
- back: "بازگشت"
- export: "خروجی گرفتن"
- name: "نام"
- description: "توضیح"
- summary: "خلاصه"
- save: "ذخیره"
- remove_parameter: "پاک کردن"
- parameter_required: "مورد نیاز"
- edit: "ویرایش"
- delete: "حذف"
- llms:
- display_name: "نام"
- save: "ذخیره"
- edit: "ویرایش"
- back: "بازگشت"
- delete: حذف
- quotas:
- group: "گروه"
- max_usages: "حداکثر استفاده"
- duration: "مدت زمان"
- durations:
- hour: "۱ ساعت"
- day: "۲۴ ساعت"
- week: "۷ روزه"
- custom: "سفارشی..."
- hours: "ساعت"
- usage:
- ai_summarization: "خلاصه کنید"
- ai_spam: "هرزنامه"
- next:
- title: "بعدی"
- tests:
- running: "در حال اجرای آزمایش..."
- success: "موفقیت!"
- failure: "در حال ارتباط با مدل این خطا رخ داد: %{error}"
- providers:
- google: "گوگل"
- fake: "سفارشی"
- ai_helper:
- title: "پیشنهاد تغییرات با استفاده از هوش مصنوعی"
- description: "یکی از گزینههای زیر را انتخاب کنید و هوش مصنوعی نسخه جدیدی از متن را به شما پیشنهاد میکند."
- selection_hint: "نکته: شما همچنین میتوانید بخشی از متن را قبل از باز کردن راهنما انتخاب کنید، تا فقط آن قسمت بازنویسی شود."
- context_menu:
- cancel: "انصراف"
- confirm: "تایید"
- discard: "حذف"
- post_options_menu:
- close: "بستن"
- copy: "کپی"
- copied: "کپی شد!"
- cancel: "انصراف"
- insert_footnote: "افزودن پاورقی"
- footnote_credits: "توضیح توسط هوش مصنوعی"
- thumbnail_suggestions:
- select: "انتخاب"
- image_caption:
- save_caption: "ذخیره"
- automatic_caption_dialog:
- confirm: "فعال کردن"
- reviewables:
- model_used: "مدل مورد استفاده:"
- accuracy: "دقت:"
- embeddings:
- back: "بازگشت"
- save: "ذخیره"
- delete: "حذف"
- edit: "ویرایش"
- tests:
- running: "در حال اجرای آزمایش..."
- success: "موفقیت!"
- display_name: "نام"
- providers:
- google: "گوگل"
- fake: "سفارشی"
- provider_fields:
- model_name: "نام مدل"
- semantic_search: "موضوعات (معنایی)"
- ai_bot:
- llm: "مدل"
- cancel_streaming: "توقف پاسخ"
- conversation_shared: "گفتگو کپی شد"
- debug_ai_modal:
- request: "درخواست"
- response: "پاسخ"
- next_log: "بعدی"
- previous_log: "پیشین"
- invite_ai_conversation:
- button: "دعوت"
- ai_label: "هوش مصنوعی"
- share_modal:
- copy: "کپی"
- bot_names:
- gpt-4: "GPT-4"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- conversations:
- today: "امروز"
- last_7_days: "۷ روز گذشته"
- last_30_days: "۳۰ روز گذشته"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "همه"
- group_types:
- category: "دستهبندی"
- tag: "برچسب"
- table:
- total_count: "مجموع"
- summarization:
- chat:
- summarize: "خلاصه کنید"
- discobot_discoveries:
- regular_results: "موضوعات"
- collapse: "جمع کردن"
- tooltip:
- actions:
- disable: "غیرفعال"
diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml
deleted file mode 100644
index 3e6e7229..00000000
--- a/config/locales/client.fi.yml
+++ /dev/null
@@ -1,686 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-fi:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Sallii tekoälyhaun"
- stream_completion: "Sallii tekoälypersoonan valmistumisen suoratoiston"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "Tunne"
- description: "Taulukossa luetellaan määritetyllä tunteella luokiteltujen viestien lukumäärä. Luokiteltu mallilla \"SamLowe/roberta-base-go_emotions\"."
- reports:
- filters:
- sort_by:
- label: "Järjestä"
- tag:
- label: "Tunniste"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Lähettäjä"
- description: "Käyttäjä, joka lähettää raportin"
- receivers:
- label: "Vastaanottajat"
- description: "Käyttäjät, jotka saavat raportin (sähköpostit lähetetään suorina sähköposteina, käyttäjätunnuksille lähetetään yksityisviesti)"
- topic_id:
- label: "Ketjun tunnus"
- description: "Sen ketjun tunnus, johon raportti julkaistaan"
- title:
- label: "Otsikko"
- description: "Raportin otsikko"
- days:
- label: "Päivät"
- description: "Raportin aikaväli"
- offset:
- label: "Siirtymä"
- description: "Testatessasi voi olla hyvä idea laatia raportti historiallisesti, käyttää siirtymää ja aloittaa raportti aikaisemmasta päivämäärästä"
- instructions:
- label: "Ohjeet"
- description: "Suurelle kielimallille annetut ohjeet"
- sample_size:
- label: "Otoksen koko"
- description: "Viestin määrä raportin otantaa varten"
- tokens_per_post:
- label: "Saneita viestiä kohden"
- description: "Viestiä kohden käytettävien suuren kielimallin saneiden määrä"
- model:
- label: "Malli"
- description: "Raportin luomiseen käytettävä LLM"
- categories:
- label: "Luokat"
- description: "Suodata ketjut vain näihin alueisiin"
- tags:
- label: "Tunnisteet"
- description: "Suodata ketjut vain näihin tunnisteisiin"
- exclude_tags:
- label: "Sulje pois tunnisteet"
- description: "Sulje pois ketjut, joilla on näitä tunnisteita"
- exclude_categories:
- label: "Sulje pois alueita"
- description: "Sulje pois näiden alueiden ketjut"
- allow_secure_categories:
- label: "Salli suojatut alueet"
- description: "Salli raportin luominen suojattujen alueiden ketjuista"
- suppress_notifications:
- label: "Estä ilmoitukset"
- description: "Estä ilmoitukset, joita raportti voi luoda muuttumalla sisällöksi. Tämä kartoittaa maininnat ja sisäiset linkit uudelleen."
- debug_mode:
- label: "Virheenkorjaustila"
- description: "Ota virheenkorjaustila käyttöön nähdäksesi LLM:n raakasyötteen ja -tuotoksen"
- priority_group:
- label: "Prioriteettiryhmä"
- description: "Priorisoi tämän ryhmän sisältö raportissa"
- temperature:
- label: "Lämpötila"
- top_p:
- label: "Top-p"
- llm_tool_triage:
- fields:
- model:
- label: "Malli"
- llm_triage:
- fields:
- system_prompt:
- label: "Järjestelmäkehote"
- description: "Kehote, jota käytetään luokitteluun; varmista, että se vastaa yhdellä sanalla, jota voit käyttää toiminnon käynnistämiseen"
- max_post_tokens:
- label: "Viestin saneiden enimmäismäärä"
- description: "Skannattavien saneiden enimmäismäärä LLM-luokittelulla"
- stop_sequences:
- label: "Pysäytysjaksot"
- description: "Ohjeista mallia keskeyttämään saneiden luominen, kun jokin näistä arvoista saavutetaan"
- search_for_text:
- label: "Hae tekstiä"
- description: "Jos suuren kielimallin vastauksessa näkyy seuraava teksti, tee nämä toimet"
- category:
- label: "Luokka"
- description: "Ketjuun sovellettava alue"
- tags:
- label: "Tunnisteet"
- description: "Ketjuun sovellettavat tunnisteet"
- canned_reply:
- label: "Vastaa"
- description: "Ketjuun lähetettävän valmiin vastauksen raaka teksti"
- canned_reply_user:
- label: "Vastauskäyttäjä"
- description: "Valmiin vastauksen lähettävän käyttäjän käyttäjätunnus"
- hide_topic:
- label: "Piilota ketju"
- description: "Tee ketjusta näkymätön yleisölle, jos tämä laukaistaan"
- flag_type:
- label: "Lipun tyyppi"
- description: "Viestiin sovellettavan lipun tyyppi (roskaposti tai yksinkertaisesti ilmoita käsiteltäväksi)"
- flag_post:
- label: "Liputa viesti"
- description: "Liputtaa viestin (joko roskapostiksi tai käsiteltäväksi)"
- include_personal_messages:
- label: "Sisällytä yksityisviestit"
- description: "Skannaa ja luokittele myös yksityisviestit"
- model:
- label: "Malli"
- description: "Luokitteluun käytettävä kielimalli"
- temperature:
- label: "Lämpötila"
- discourse_ai:
- title: "Tekoäly"
- features:
- back: "Takaisin"
- disabled: "(pois käytöstä)"
- groups: "Ryhmät:"
- no_persona: "Ei asetettu"
- no_groups: "Ei valittu"
- edit: "Muokkaa"
- expand_list:
- one: "(%{count} muuta)"
- other: "(%{count} muuta)"
- collapse_list: "(näytä vähemmän)"
- filters:
- all: "Kaikki"
- reset: "Palauta"
- search:
- name: "Hae"
- embeddings:
- name: "Upotukset"
- ai_helper:
- name: "Apuri"
- proofread: Oikolue teksti
- explain: "Selitä"
- smart_dates: "Älykkäät päivämäärät"
- markdown_tables: "Luo markdown-taulukko"
- custom_prompt: "Mukautettu kehote"
- spam:
- name: "Roskaposti"
- description: "Tunnistaa mahdollisen roskapostin valitun suuren kielimallin avulla ja liputtaa sen sivuston valvojien tarkastettavaksi tarkastusjonoon"
- modals:
- select_option: "Valitse vaihtoehto..."
- spam:
- short_title: "Roskaposti"
- title: "Määritä roskapostin käsittely"
- select_llm: "Valitse LLM"
- custom_instructions: "Mukautetut ohjeet"
- custom_instructions_help: "Sivustoasi koskevat mukautetut ohjeet, jotka auttavat tekoälyä tunnistamaan roskapostia, esim. \"ole aggressiivisempi skannattaessa viestejä, jotka eivät ole englanninkielisiä\"."
- last_seven_days: "Viimeiset 7 päivää"
- scanned_count: "Skannatut viestit"
- false_positives: "Liputettu väärin"
- false_negatives: "Huomaamatta jäänyt roskaposti"
- spam_detected: "Havaittu roskaposti"
- custom_instructions_placeholder: "Sivustokohtaiset ohjeet, jotka auttavat tekoälyä tunnistamaan roskapostin tarkemmin"
- enable: "Ota käyttöön"
- spam_tip: "Tekoälyyn perustuva roskapostin tunnistus skannaa kaikkien uusien käyttäjien kolme ensimmäistä viestiä julkisissa ketjuissa. Se liputtaa ne käsiteltäväksi ja estää käyttäjät, jos ne ovat todennäköisesti roskapostia."
- settings_saved: "Asetukset tallennettu"
- spam_description: "Tunnistaa mahdollisen roskapostin valitun suuren kielimallin avulla ja liputtaa sen sivuston valvojien tarkastettavaksi tarkastusjonoon"
- no_llms: "Suuria kielimalleja ei ole saatavilla"
- test_button: "Testaa..."
- save_button: "Tallenna muutokset"
- test_modal:
- title: "Testaa roskapostin tunnistusta"
- post_url_label: "Vistin URL tai tunnus"
- post_url_placeholder: "https://foorumisi.com/t/topic/123/4 tai viestin tunnus"
- result: "Tulos"
- scan_log: "Skannausloki"
- run: "Suorita testi"
- spam: "Roskaposti"
- not_spam: "Ei roskapostia"
- stat_tooltips:
- incorrectly_flagged: "Kohteet, jotka tekoälybotti liputti roskapostiksi, mutta joista valvojat olivat eri mieltä"
- missed_spam: "Yhteisön roskapostiksi liputtamaton kohteet, joita tekoälybotti ei havainnut ja joista valvojat olivat samaa mieltä"
- errors:
- scan_not_admin:
- message: "Varoitus: roskapostin skannaus ei toimi oikein, koska roskapostin skannaustili ei ole ylläpitäjä"
- action: "Korjaa"
- resolved: "Virhe on korjattu!"
- usage:
- short_title: "Käyttö"
- summary: "Yhteenveto"
- total_tokens: "Saneet yhteensä"
- tokens_over_time: "Saneet ajan myötä"
- features_breakdown: "Käyttö ominaisuutta kohden"
- feature: "Ominaisuus"
- usage_count: "Käyttömäärä"
- model: "Malli"
- models_breakdown: "Käyttö mallia kohden"
- users_breakdown: "Käyttö käyttäjää kohden"
- all_features: "Kaikki ominaisuudet"
- all_models: "Kaikki mallit"
- username: "Käyttäjätunnus"
- total_requests: "Pyyntöjä yhteensä"
- request_tokens: "Pyynnön tokenit"
- response_tokens: "Vastauksen tokenit"
- net_request_tokens: "Nettopyyntösaneet"
- cached_tokens: "Välimuistissa olevat saneet"
- cached_request_tokens: "Välimuistiin tallennetut pyyntösaneet"
- no_users: "Käyttäjien käyttötietoja ei löytynyt"
- no_models: "Mallin käyttötietoja ei löytynyt"
- no_features: "Ominaisuuden käyttötietoja ei löytynyt"
- subheader_description: "Saneet ovat perusyksiköitä, joita suuret kielimallit käyttävät tekstin ymmärtämiseen ja luomiseen. Käyttötiedot voivat vaikuttaa kustannuksiin"
- stat_tooltips:
- total_requests: "Kaikki suurille kielimalleille Discoursen kautta tehdyt pyynnöt"
- total_tokens: "Kaikki käytetyt saneet kehotteissa suurille kielimalleille"
- request_tokens: "Käytetyt saneet, kun suuri kielimalli yrittää ymmärtää, mitä sanot"
- response_tokens: "Käytetyt saneet, kun suuri kielimalli vastaa kehotteeseesi"
- cached_tokens: "Aiemmin käsitellyt pyyntösaneet, joita suuri kielimalli käyttää uudelleen suorituskyvyn ja kustannusten optimoimiseksi"
- periods:
- last_day: "Viimeiset 24 tuntia"
- last_week: "Viime viikko"
- last_month: "Viime kuukausi"
- custom: "Mukautettu..."
- ai_persona:
- ai_tools: "Työkalut"
- tool_strategies:
- all: "Käytä kaikkiin vastauksiin"
- replies:
- one: "Käytä vain ensimmäiseen vastaukseen"
- other: "Käytä vain %{count} ensimmäiseen vastaukseen"
- back: "Takaisin"
- name: "Nimi"
- edit: "Muokkaa"
- export: "Vie"
- description: "Kuvaus"
- no_llm_selected: "Kielimallia ei ole valittu"
- max_context_posts: "Kontekstiviestien enimmäismäärä"
- max_context_posts_help: "Kontekstina käytettävien viestien enimmäismäärä, kun tekoäly vastaa käyttäjälle (käytä oletusta jättämällä tyhjäksi)"
- vision_enabled: Näkö käytössä
- vision_enabled_help: Jos tämä on käytössä, tekoäly yrittää ymmärtää kuvia, joita käyttäjät julkaisevat ketjussa, riippuen siitä, tukeeko käytettävä malli näköä. Anthropicin, Googlen ja OpenAI:n uusimmat mallit tukevat näköä.
- vision_max_pixels: Tuettu kuvakoko
- vision_max_pixel_sizes:
- low: Heikko laatu – halvin (256x256)
- medium: Keskilaatuinen (512x512)
- high: Korkea laatu – hitain (1024x1024)
- tool_details: Näytä työkalun tiedot
- tool_details_help: Näyttää loppukäyttäjille tiedot siitä, mitkä työkalut kielimalli on laukaissut.
- mentionable: Salli maininnat
- mentionable_help: Jos tämä on käytössä, sallittujen ryhmien käyttäjät voivat mainita tämän käyttäjän viesteissä, tekoäly vastaa tänä persoonana.
- user: Käyttäjä
- create_user: Luo käyttäjä
- create_user_help: Voit halutessasi liittää käyttäjän tähän persoonaan. Jos teet näin, tekoäly vastaa pyyntöihin käyttämällä tätä käyttäjää.
- default_llm: Oletuskielimalli
- default_llm_help: Tälle persoonalle käytettävä oletuskielimalli. Vaaditaan, jos haluat mainita persoonan julkisissa viesteissä.
- question_consolidator_llm: Kielimalli kysymysten yhdistäjälle
- question_consolidator_llm_help: Kysymysten yhdistäjälle käytettävä kielimalli, voit valita vähemmän tehokkaan mallin kustannusten säästämiseksi.
- system_prompt: Järjestelmäkehote
- forced_tool_strategy: Pakotetun työkalun strategia
- allow_chat_direct_messages: "Salli chat-yksityisviestit"
- allow_chat_direct_messages_help: "Jos tämä on käytössä, sallittujen ryhmien käyttäjät voivat lähettää yksityisviestejä tälle persoonalle."
- allow_chat_channel_mentions: "Salli chat-kanavamaininnat"
- allow_chat_channel_mentions_help: "Jos tämä on käytössä, sallittujen ryhmien käyttäjät voivat mainita tämän persoonan chat-kanavilla."
- allow_personal_messages: "Salli yksityisviestit"
- allow_personal_messages_help: "Jos tämä on käytössä, sallittujen ryhmien käyttäjät voivat lähettää yksityisviestejä tälle persoonalle."
- allow_topic_mentions: "Salli ketjumaininnat"
- allow_topic_mentions_help: "Jos tämä on käytössä, sallittujen ryhmien käyttäjät voivat mainita tämän persoonan ketjuissa."
- force_default_llm: "Käytä aina oletuskielimallia"
- save: "Tallenna"
- saved: "Persoona tallennettu"
- enabled: "Otettu käyttöön?"
- tools: "Käytössä olevat työkalut"
- forced_tools: "Pakotetut työkalut"
- allowed_groups: "Sallitut ryhmät"
- confirm_delete: "Oletko varma, että haluat poistaa tämän persoonan?"
- new: "Uusi persoona"
- no_personas: "Et ole vielä luonut persoonia"
- title: "Persoonat"
- short_title: "Persoonat"
- delete: "Poista"
- temperature: "Lämpötila"
- temperature_help: "LLM:lle käytettävä lämpötila. Lisää luovuutta kasvattamalla arvoa (jätä tyhjäksi, jos haluat käyttää mallin oletusta, yleensä arvo välillä 0,0–2,0)"
- top_p: "Top-p"
- top_p_help: "LLM:lle käytettävä top-p, lisää satunnaisuutta kasvattamalla arvoa (jätä tyhjäksi, jos haluat käyttää mallin oletusta, yleensä arvo välillä 0,0–1,0)"
- priority: "Prioriteetti"
- priority_help: "Prioriteettipersoonat näytetään käyttäjille ensimmäisinä persoonaluettelossa. Jos useilla persoonilla on prioriteetti, ne järjestetään aakkosjärjestyksessä."
- tool_options: "Työkaluasetukset"
- rag_conversation_chunks: "Hakukeskustelulohkot"
- rag_conversation_chunks_help: "RAG-mallin hauissa käytettävien lohkojen määrä. Lisää kontekstin määrää, jota tekoäly voi käyttää, kasvattamalla arvoa."
- persona_description: "Personat ovat tehokas ominaisuus, jonka avulla voit mukauttaa tekoälymoduulin toimintaa Discourse-foorumillasi. Ne toimivat \"järjestelmäviestinä\", joka ohjaa tekoälyn vastauksia ja vuorovaikutusta ja auttaa luomaan personoidumman ja kiinnostavamman käyttökokemuksen."
- response_format:
- open_modal: "Muokkaa"
- modal:
- key_title: "Avain"
- filters:
- reset: "Palauta"
- rag:
- options:
- rag_chunk_tokens: "Latauslohkotokenit"
- rag_chunk_tokens_help: "Kullekin lohkolle käytettävien tokenien määrä RAG-mallissa. Lisää kontekstin määrää, jota tekoäly voi käyttää, kasvattamalla arvoa (muuttaminen indeksoi kaikki lataukset palvelimeen uudelleen)."
- rag_chunk_overlap_tokens: "Latauslohkon päällekkäiset tokenit"
- rag_chunk_overlap_tokens_help: "Päällekkäisten tokenien määrä lohkojen välillä RAG-mallissa (muuttaminen indeksoi kaikki lataukset palvelimeen uudelleen). "
- show_indexing_options: "Näytä latausasetukset"
- hide_indexing_options: "Piilota latausasetukset"
- uploads:
- title: "Lataukset"
- button: "Lisää tiedostoja"
- filter: "Suodata latauksia"
- indexed: "Indeksoitu"
- indexing: "Indeksointi"
- uploaded: "Valmiina indeksoitavaksi"
- uploading: "Ladataan..."
- remove: "Poista lataus"
- tools:
- back: "Takaisin"
- short_title: "Työkalut"
- export: "Vie"
- no_tools: "Et ole vielä luonut työkaluja"
- name: "Nimi"
- new: "Uusi työkalu"
- description: "Kuvaus"
- description_help: "Selkeä kuvaus työkalun tarkoituksesta kielimallissa"
- subheader_description: "Työkalut laajentavat tekoälybottien ominaisuuksia käyttäjän määrittämillä JavaScript-toiminnoilla."
- summary: "Yhteenveto"
- summary_help: "Loppukäyttäjille näytettävä yhteenveto työkalujen tarkoituksesta"
- script: "Skripti"
- parameters: "Parametrit"
- save: "Tallenna"
- remove_parameter: "Poista"
- parameter_required: "Pakollinen"
- parameter_enum: "Enum"
- parameter_name: "Parametrin nimi"
- parameter_description: "Parametrin kuvaus"
- enum_value: "Enum-arvo"
- add_enum_value: "Lisää enum-arvo"
- edit: "Muokkaa"
- test: "Suorita testi"
- delete: "Poista"
- saved: "Työkalu tallennettu"
- confirm_delete: "Oletko varma, että haluat poistaa tämän työkalun?"
- test_modal:
- title: "Testaa tekoälytyökalua"
- run: "Suorita testi"
- result: "Testin tulos"
- llms:
- short_title: "LLM:t"
- no_llms: "LLM:iä ei ole vielä"
- new: "Uusi malli"
- display_name: "Nimi"
- name: "Mallin tunnus"
- provider: "Palveluntarjoaja"
- tokenizer: "Tokenisoija"
- url: "Mallia isännöivän palvelun URL-osoite"
- api_key: "Mallia isännöivän palvelun API-avain"
- enabled_chat_bot: "Salli tekoälybotin valitsin"
- vision_enabled: "Näkö käytössä"
- ai_bot_user: "Tekoälybottikäyttäjä"
- save: "Tallenna"
- edit: "Muokkaa"
- saved: "LLM-malli tallennettu"
- back: "Takaisin"
- confirm_delete: Oletko varma, että haluat poistaa tämän mallin?
- delete: Poista
- seeded_warning: "Tämä malli on valmiiksi määritetty sivustollesi, eikä sitä voi muokata."
- quotas:
- title: "Käyttökiintiöt"
- add_title: "Luo uusi kiintiö"
- group: "Ryhmä"
- max_tokens: "Saneiden enimmäismäärä"
- max_usages: "Käyttökertoja enintään"
- duration: "Kesto"
- confirm_delete: "Oletko varma, että haluat poistaa tämän kiintiön?"
- add: "Lisää kiintiö"
- durations:
- hour: "1 tunti"
- six_hours: "6 tuntia"
- day: "24 tuntia"
- week: "7 päivää"
- custom: "Mukautettu..."
- hours: "tuntia"
- max_tokens_help: "Saneiden (sanojen ja merkkien) enimmäismäärä, jonka kukin tämän ryhmän käyttäjä voi käyttää määritetyn keston aikana. Saneet ovat yksiköitä, joita tekoälymallit käyttävät tekstin käsittelyyn – noin 1 merkki = 4 merkkiä tai 3/4 sanasta."
- max_usages_help: "Enimmäismäärä kertoja, jonka kukin tämän ryhmän käyttäjä voi käyttää tekoälymallia määritetyn ajan kuluessa. Tätä kiintiötä seurataan yksittäistä käyttäjää kohti, eikä sitä jaeta ryhmän kesken."
- usage:
- ai_bot: "Tekoälyrobotti"
- ai_helper: "Apuri"
- ai_persona: "Persoona (%{persona})"
- ai_summarization: "Tee yhteenveto"
- ai_embeddings_semantic_search: "Tekoälyhaku"
- ai_spam: "Roskaposti"
- in_use_warning:
- one: "Tätä mallia käyttää tällä hetkellä %{settings}. Jos se on määritetty väärin, ominaisuus ei toimi odotetulla tavalla."
- other: "Tätä mallia käyttää tällä hetkellä seuraavat: %{settings}. Jos se on määritetty väärin, ominaisuus ei toimi odotetulla tavalla. "
- model_description:
- none: "Yleiset asetukset, jotka toimivat useimmissa kielimalleissa"
- anthropic-claude-opus-4-0: "Anthropicin älykkäin malli"
- anthropic-claude-3-5-haiku-latest: "Nopea ja kustannustehokas"
- google-gemini-2-5-flash: "Kevyt, nopea ja kustannustehokas multimodaalisella päättelyllä"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Tehokas kevyt monikielinen malli"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Tehokas monikäyttöinen malli"
- mistral-mistral-large-latest: "Mistralin tehokkain malli"
- mistral-pixtral-large-latest: "Mistralin tehokkain näkökykyinen malli"
- preseeded_model_description: "Ennalta määritetty avoimen lähdekoodin malli, joka käyttää mallia %{model}"
- configured:
- title: "Määritetyt LLM:t"
- preconfigured_llms: "Valitse LLM"
- preconfigured:
- title_no_llms: "Aloita valitsemalla malli"
- title: "Määrittämättömät LLM-mallit"
- description: "LLM:t (suuret kielimallit) ovat tekoälytyökaluja, jotka on optimoitu tehtäviin, kuten sisällön yhteenvedon laatimiseen sisällöstä, raporttien luomiseen, asiakasvuorovaikutusten automatisointiin sekä foorumin valvonnan ja tietokatsausten helpottamiseen"
- fake: "Manuaalinen määritys"
- button: "Määritä"
- next:
- title: "Seuraava"
- tests:
- title: "Suorita testi"
- running: "Suoritetaan testiä..."
- success: "Onnistui!"
- failure: "Yritys ottaa yhteyttä malliin palautti tämän virheen: %{error}"
- hints:
- name: "Sisällytämme tämän API-kutsuun määrittääksemme, mitä mallia käytämme"
- vision_enabled: "Jos tämä on käytössä, tekoäly yrittää ymmärtää kuvia. Tämä riippuu siitä, tukeeko käytettävä malli näköä. Anthropicin, Googlen ja OpenAI:n uusimmat mallit tukevat näköä."
- enabled_chat_bot: "Jos tämä on käytössä, käyttäjät voivat valita tämän mallin luodessaan yksityisviestejä tekoälybotin kanssa"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Mukautettu"
- provider_fields:
- access_key_id: "AWS Bedrockin käyttöavaintunnus"
- region: "AWS Bedrockin alue"
- organization: "Valinnainen OpenAI:n organisaatiotunnus"
- disable_system_prompt: "Poista järjestelmäviesti käytöstä kehotteissa"
- enable_native_tool: "Ota käyttöön natiivityökalujen tuki"
- disable_native_tools: "Poista natiivityökalujen tuki käytöstä (käytä XML-pohjaisia työkaluja)"
- provider_order: "Palveluntarjoajien järjestys (pilkuilla eroteltu luettelo)"
- provider_quantizations: "Palveluntarjoajien kvantisointien järjestys (pilkuilla eroteltu luettelo esim.: fp16,fp8)"
- disable_streaming: "Poista suoratoistovalmistumiset käytöstä (muunna suoratoistopyynnöt ei-suoratoistopyynnöiksi)"
- related_topics:
- title: "Liittyvät ketjut"
- pill: "Liittyy aiheeseen"
- ai_helper:
- title: "Ehdota muutoksia tekoälyn avulla"
- description: "Valitse yksi seuraavista vaihtoehdoista, niin tekoäly ehdottaa sinulle uutta versiota tekstistä."
- selection_hint: "Vihje: voit myös valita osan tekstistä ennen apuohjelman avaamista kirjoittaaksesi vain sen uudelleen."
- suggest: "Ehdota tekoälyllä"
- suggest_errors:
- too_many_tags:
- one: "Sinulla voi olla enintään %{count} tunniste"
- other: "Sinulla voi olla enintään %{count} tunnistetta"
- no_suggestions: "Ei ehdotuksia saatavilla"
- missing_content: "Anna sisältöä ehdotusten luomiseksi."
- context_menu:
- trigger: "Kysy tekoälyltä"
- loading: "Tekoäly tuottaa"
- cancel: "Peruuta"
- confirm: "Vahvista"
- discard: "Hylkää"
- changes: "Ehdotetut muokkaukset"
- custom_prompt:
- title: "Mukautettu kehote"
- placeholder: "Anna mukautettu kehote..."
- submit: "Lähetä kehote"
- translate_prompt: "Käännä kielelle %{language}"
- post_options_menu:
- trigger: "Kysy tekoälyltä"
- title: "Kysy tekoälyltä"
- loading: "Tekoäly tuottaa"
- close: "Sulje"
- copy: "Kopioi"
- copied: "Kopioitiin!"
- cancel: "Peruuta"
- insert_footnote: "Lisää alaviite"
- footnote_disabled: "Automaattinen lisäys poistettu käytöstä, napsauta kopiointipainiketta ja lisää se manuaalisesti"
- footnote_credits: "Tekoälyn selitys"
- fast_edit:
- suggest_button: "Ehdota muokkausta"
- thumbnail_suggestions:
- title: "Ehdotetut pikkukuvat"
- select: "Valitse"
- selected: "Valittu"
- image_caption:
- button_label: "Kuvateksti tekoälyllä"
- generating: "Luodaan kuvatekstiä..."
- credits: "Tekoälyn laatima kuvateksti"
- save_caption: "Tallenna"
- automatic_caption_setting: "Ota automaattinen kuvateksti käyttöön"
- automatic_caption_loading: "Luodaan kuvatekstejä kuville..."
- automatic_caption_dialog:
- prompt: "Tämä viesti sisältää kuvia, joilla ei ole kuvatekstiä. Haluatko ottaa automaattiset kuvatekstit käyttöön ladattaessa kuvia verkkoon? (Voit muuttaa tätä myöhemmin asetuksissasi)"
- confirm: "Ota käyttöön"
- cancel: "Älä kysy uudelleen"
- no_content_error: "Lisää ensin sisältöä tehdäksesi tekoälytoimia sille"
- reviewables:
- model_used: "Käytetty malli:"
- accuracy: "Tarkkuus:"
- embeddings:
- short_title: "Upotukset"
- new: "Uusi upotus"
- back: "Takaisin"
- save: "Tallenna"
- saved: "Upotusmääritykset tallennettu"
- delete: "Poista"
- confirm_delete: Haluatko varmasti poistaa tämän upotusmäärityksen?
- empty: "Et ole vielä määrittänyt upotuksia"
- presets: "Valitse esiasetus..."
- configure_manually: "Määritä manuaalisesti"
- edit: "Muokkaa"
- seeded_warning: "Tämä on valmiiksi määritetty sivustollesi, eikä sitä voi muokata."
- tests:
- title: "Suorita testi"
- running: "Suoritetaan testiä..."
- success: "Onnistui!"
- failure: "Yritys luoda upotus johti seuraavaan tulokseen: %{error}"
- hints:
- dimensions_warning: "Tallentamisen jälkeen tätä arvoa ei voi enää muuttaa."
- matryoshka_dimensions: "Määrittää sellaisten sisäkkäisten upotusten koon, joita käytetään tietojen hierarkkiseen tai monikerroksiseen esittämiseen samalla tavalla kuin maatuskanuket sopivat toisiinsa."
- sequence_length: "Enimmäismäärä saneita, joka voidaan käsitellä kerralla luotaessa upotuksia tai käsiteltäessä kyselyä."
- distance_function: "Määrittää, kuinka upotusten välinen samankaltaisuus lasketaan, käyttämällä joko kosinietäisyyttä (mittaamalla vektorien välisen kulman) tai negatiivista sisätuloa (mittaamalla vektorin arvojen päällekkäisyyttä)."
- display_name: "Nimi"
- provider: "Palveluntarjoaja"
- url: "Upotuspalvelun URL"
- api_key: "Upotuspalvelun API-avain"
- tokenizer: "Tokenisoija"
- dimensions: "Upotusmitat"
- max_sequence_length: "Jakson pituus"
- embed_prompt: "Upotuskehote"
- search_prompt: "Hakukehote"
- matryoshka_dimensions: "Maatuskamitat"
- distance_function: "Etäisyysfunktio"
- distance_functions:
- "<#>": "Negatiivinen sisätulo"
- <=>: "Kosinietäisyys"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Mukautettu"
- provider_fields:
- model_name: "Mallin nimi"
- semantic_search: "Ketjut (semanttinen)"
- semantic_search_loading: "Haetaan lisää tuloksia tekoälyllä"
- semantic_search_results:
- toggle: "Näytetään %{count} tulosta, jotka löydettiin tekoälyllä"
- toggle_hidden: "Piilotetaan %{count} tulosta, jotka löydettiin tekoälyllä"
- none: "Tekoälyhakumme ei valitettavasti löytänyt vastaavia aiheita"
- new: "Aloita uusien tulosten etsiminen tekoälyllä painamalla hakupainiketta"
- unavailable: "Tekoälytuloksia ei ole saatavilla"
- semantic_search_tooltips:
- results_explanation: "Kun tämä on käytössä, alle lisätään ylimääräisiä tekoälyhakutuloksia."
- invalid_sort: "Hakutulokset täytyy olla järjestetty osuvuuden mukaan, jotta tekoälytuloksia voidaan näyttää"
- semantic_search_unavailable_tooltip: "Hakutulokset täytyy olla järjestetty osuvuuden mukaan, jotta tekoälytuloksia voidaan näyttää"
- ai_generated_result: "Tekoälyllä löydetty hakutulos"
- quick_search:
- suffix: "kaikissa ketjuissa ja viesteissä tekoälyllä"
- ai_artifact:
- expand_view_label: "Laajenna näkymä"
- collapse_view_label: "Poistu koko näytön tilasta (Esc- tai Takaisin-painike)"
- click_to_run_label: "Suorita artefakti"
- ai_bot:
- llm: "Malli"
- pm_warning: "Valvojat tarkkailevat säännöllisesti tekoälychatbotin viestejä"
- cancel_streaming: "Lopeta vastaus"
- default_pm_prefix: "[Nimetön tekoälybotin yksityisviesti]"
- shortcut_title: "Aloita yksityiskeskustelu tekoälybotin kanssa"
- share: "Kopioi tekoälykeskustelu"
- conversation_shared: "Keskustelu kopioitu"
- debug_ai: "Katso raaka tekoälypyyntö ja vastaus"
- debug_ai_modal:
- title: "Näytä tekoälyvuorovaikutus"
- copy_request: "Kopioi pyyntö"
- copy_response: "Kopioi vastaus"
- request_tokens: "Pyynnön tokenit:"
- response_tokens: "Vastauksen tokenit:"
- request: "Pyyntö"
- response: "Vastaus"
- next_log: "Seuraava"
- previous_log: "Edellinen"
- share_full_topic_modal:
- title: "Jaa keskustelu julkisesti"
- share: "Jaa ja kopioi linkki"
- update: "Päivitä ja kopioi linkki"
- delete: "Poista jako"
- share_ai_conversation:
- name: "Jaa tekoälykeskustelu"
- title: "Jaa tämä tekoälykeskustelu julkisesti"
- invite_ai_conversation:
- button: "Kutsu"
- ai_label: "Tekoäly"
- ai_title: "Keskustelu tekoälyn kanssa"
- share_modal:
- title: "Kopioi tekoälykeskustelu"
- copy: "Kopioi"
- context: "Jaettavat vuorovaikutukset:"
- share_tip: "Vaihtoehtoisesti voit jakaa koko keskustelun"
- bot_names:
- fake: "Valetestibotti"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "Tänään"
- last_7_days: "Viimeiset 7 päivää"
- last_30_days: "Viimeiset 30 päivää"
- sentiments:
- dashboard:
- title: "Tunne"
- sentiment_analysis:
- filter_types:
- all: "Kaikki"
- positive: "Positiivinen"
- neutral: "Neutraali"
- negative: "Negatiivinen"
- group_types:
- category: "Luokka"
- tag: "Tunniste"
- table:
- sentiment: "Tunne"
- total_count: "Yhteensä"
- summarization:
- chat:
- title: "Tee yhteenveto viesteistä"
- description: "Valitse vaihtoehto alla yhteenvedon tekemiseksi haluttuna ajanjaksona lähetetystä keskustelusta."
- summarize: "Tee yhteenveto"
- since:
- one: "Viimeinen tunti"
- other: "Viimeiset %{count} tuntia"
- topic:
- title: "Ketjun yhteenveto"
- close: "Sulje yhteenvetopaneeli"
- topic_list_layout:
- button:
- compact: "Kompakti"
- expanded: "Laajennettu"
- expanded_description: "tekoäly-yhteenvedoilla"
- discobot_discoveries:
- regular_results: "Aiheet"
- collapse: "Kutista"
- tooltip:
- actions:
- disable: "Poista käytöstä"
- review:
- types:
- reviewable_ai_post:
- title: "Tekoälyn liputtama viesti"
- reviewable_ai_chat_message:
- title: "Tekoälyn liputtama chat-viesti"
diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml
deleted file mode 100644
index b611a060..00000000
--- a/config/locales/client.fr.yml
+++ /dev/null
@@ -1,686 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-fr:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Permet la recherche alimentée par l'IA"
- stream_completion: "Permet de diffuser en continu les complétions de personnages IA"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "Émotion"
- description: "Le tableau répertorie le nombre de publications classées selon une émotion déterminée. Cette classification est réalisée avec le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- reports:
- filters:
- sort_by:
- label: "Trier par"
- tag:
- label: "Étiqueter"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Expéditeur"
- description: "L'utilisateur qui enverra le rapport"
- receivers:
- label: "Récepteurs"
- description: "Les utilisateurs qui recevront le rapport (les e-mails seront envoyés directement par e-mail, les noms d'utilisateur seront envoyés par MP)"
- topic_id:
- label: "ID du sujet"
- description: "L'ID du sujet dans lequel publier le rapport"
- title:
- label: "Titre"
- description: "Le titre du rapport"
- days:
- label: "Jours"
- description: "La durée du rapport"
- offset:
- label: "Décalage"
- description: "Lors des tests, vous souhaiterez peut-être exécuter le rapport de manière historique, utilisez le décalage pour démarrer le rapport à une date antérieure."
- instructions:
- label: "Instructions"
- description: "Les instructions fournies au modèle de langage"
- sample_size:
- label: "Taille de l'échantillon"
- description: "Le nombre de publications à échantillonner pour le rapport"
- tokens_per_post:
- label: "Jetons par publication"
- description: "Le nombre de jetons LLM à utiliser par publication"
- model:
- label: "Modèle"
- description: "LLM à utiliser pour la génération de rapports"
- categories:
- label: "Catégories"
- description: "Filtrer les sujets uniquement selon ces catégories"
- tags:
- label: "Étiquettes"
- description: "Filtrer les sujets uniquement selon ces étiquettes"
- exclude_tags:
- label: "Exclure les étiquettes"
- description: "Exclure les sujets comportant ces étiquettes"
- exclude_categories:
- label: "Exclure les catégories"
- description: "Exclure les sujets comportant ces catégories"
- allow_secure_categories:
- label: "Autoriser les catégories sécurisées"
- description: "Autoriser la génération du rapport pour les sujets classés dans des catégories sécurisées"
- suppress_notifications:
- label: "Supprimer les notifications"
- description: "Supprimez les notifications que le rapport peut générer en les transformant en contenu. Cela permettra de réorganiser les mentions et les liens internes."
- debug_mode:
- label: "Mode de débogage"
- description: "Activez le mode de débogage pour voir les entrées et sorties brutes du LLM"
- priority_group:
- label: "Groupe prioritaire"
- description: "Priorisez le contenu de ce groupe dans le rapport"
- temperature:
- label: "Température"
- top_p:
- label: "Top P"
- llm_tool_triage:
- fields:
- model:
- label: "Modèle"
- llm_triage:
- fields:
- system_prompt:
- label: "Invite du système"
- description: "L'invite qui sera utilisée pour le tri, assurez-vous qu'elle répond avec un seul mot que vous pouvez utiliser pour déclencher l'action"
- max_post_tokens:
- label: "Nombre maximum de jetons de publication"
- description: "Le nombre maximal de jetons à analyser à l'aide du triage LLM"
- stop_sequences:
- label: "Arrêter les séquences"
- description: "Demandez au modèle d'arrêter la génération de jetons lorsqu'il atteint l'une de ces valeurs"
- search_for_text:
- label: "Recherche de texte"
- description: "Si le texte suivant apparaît dans la réponse LLM, appliquez ces actions"
- category:
- label: "Catégorie"
- description: "Catégorie à appliquer au sujet"
- tags:
- label: "Étiquettes"
- description: "Étiquettes à appliquer au sujet"
- canned_reply:
- label: "Réponse"
- description: "Texte brut de la réponse prédéfinie à la publication dans le sujet"
- canned_reply_user:
- label: "Répondre à l'utilisateur"
- description: "Nom d'utilisateur de l'utilisateur qui publiera la réponse prédéfinie"
- hide_topic:
- label: "Masquer le sujet"
- description: "Rendez le sujet invisible au public si l'action est déclenchée"
- flag_type:
- label: "Type de signalement"
- description: "Type de signalement à appliquer au message (spam ou simplement marquer pour examen)"
- flag_post:
- label: "Signaler cette publication"
- description: "Signale la publication (soit comme spam ou pour examen)"
- include_personal_messages:
- label: "Inclure les messages personnels"
- description: "Analysez et triez également les messages personnels"
- model:
- label: "Modèle"
- description: "Modèle linguistique utilisé pour le tri"
- temperature:
- label: "Température"
- discourse_ai:
- title: "IA"
- features:
- back: "Retour"
- disabled: "(désactivée)"
- groups: "Groupes:"
- no_persona: "Non défini"
- no_groups: "Jamais"
- edit: "Modifier"
- expand_list:
- one: "(%{count} autres)"
- other: "(%{count} autres)"
- collapse_list: "(afficher moins)"
- filters:
- all: "Tout"
- reset: "Réinitialiser"
- search:
- name: "Recherche"
- embeddings:
- name: "Intégrations"
- ai_helper:
- name: "Assistant"
- proofread: Relisez le texte
- explain: "Expliquer"
- smart_dates: "Dates intelligentes"
- markdown_tables: "Générer un tableau Markdown"
- custom_prompt: "Invite personnalisée"
- spam:
- name: "Spam"
- description: "Identifie les spams potentiels à l'aide du LLM sélectionné et les signale aux modérateurs du site afin qu'ils les inspectent dans la file d'attente de révision"
- modals:
- select_option: "Sélectionnez une option..."
- spam:
- short_title: "Spam"
- title: "Configurer la gestion du spam"
- select_llm: "Sélectionner LLM"
- custom_instructions: "Instructions personnalisées"
- custom_instructions_help: "Des instructions personnalisées spécifiques à votre site pour aider l'IA à identifier le spam, par exemple « Faites preuve d'agressivité dans l'analyse des messages qui ne sont pas en anglais »."
- last_seven_days: "Ces 7 derniers jours"
- scanned_count: "Publications analysées"
- false_positives: "Signalé incorrectement"
- false_negatives: "Spam manqué"
- spam_detected: "Spam détecté"
- custom_instructions_placeholder: "Instructions spécifiques au site pour que l'IA puisse identifier les spams avec plus de précision"
- enable: "Activer"
- spam_tip: "La détection de spams par l'IA analysera les 3 premiers messages de tous les nouveaux utilisateurs sur les sujets publics. Elle les signalera pour examen et bloquera les utilisateurs s'ils sont susceptibles d'envoyer du spam."
- settings_saved: "Paramètres enregistrés"
- spam_description: "Identifie les spams potentiels à l'aide du LLM sélectionné et les signale aux modérateurs du site afin qu'ils les inspectent dans la file d'attente de révision"
- no_llms: "Aucun LLM disponible"
- test_button: "Test..."
- save_button: "Enregistrer les modifications"
- test_modal:
- title: "Tester la détection du spam"
- post_url_label: "URL ou ID de la publication"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 ou ID de la publication"
- result: "Résultat"
- scan_log: "Journal d'analyse"
- run: "Lancer le test"
- spam: "Spam"
- not_spam: "Non spam"
- stat_tooltips:
- incorrectly_flagged: "Éléments que le robot IA a signalés comme spam et pour lesquels les modérateurs n'étaient pas d'accord"
- missed_spam: "Éléments signalés par la communauté comme spam qui n'ont pas été détectés par le robot IA, et sur lesquels les modérateurs ont été d'accord"
- errors:
- scan_not_admin:
- message: "Avertissement : l'analyse anti-spam ne fonctionnera pas correctement, car le compte d'analyse anti-spam n'est pas un compte administrateur"
- action: "Corriger"
- resolved: "L'erreur a été résolue !"
- usage:
- short_title: "Utilisation"
- summary: "Résumé"
- total_tokens: "Total des jetons"
- tokens_over_time: "Jetons au fil du temps"
- features_breakdown: "Utilisation par fonctionnalité"
- feature: "Fonctionnalité"
- usage_count: "Nombre d'utilisations"
- model: "Modèle"
- models_breakdown: "Utilisations par modèle"
- users_breakdown: "Utilisations par utilisateur"
- all_features: "Toutes les fonctionnalités"
- all_models: "Tous les modèles"
- username: "Nom d'utilisateur"
- total_requests: "Total des requêtes"
- request_tokens: "Jetons de requête"
- response_tokens: "Jetons de réponse"
- net_request_tokens: "Jetons de requête nets"
- cached_tokens: "Jetons mis en cache"
- cached_request_tokens: "Jetons de requête en cache"
- no_users: "Aucune donnée d'utilisation de l'utilisateur n'a été trouvée"
- no_models: "Aucune donnée d'utilisation du modèle n'a été trouvée"
- no_features: "Aucune donnée d'utilisation de la fonctionnalité n'a été trouvée"
- subheader_description: "Les jetons sont les unités de base que les LLM utilisent pour comprendre et générer du texte. Les données d'utilisation peuvent affecter les coûts"
- stat_tooltips:
- total_requests: "Toutes les requêtes adressées aux LLM via Discourse"
- total_tokens: "Tous les jetons utilisés lors de la consultation d'un LLM"
- request_tokens: "Jetons utilisés lorsque le LLM essaie de comprendre ce que vous dites"
- response_tokens: "Jetons utilisés lorsque le LLM répond à votre invite"
- cached_tokens: "Jetons de requête précédemment traités que le LLM réutilise pour optimiser les performances et les coûts"
- periods:
- last_day: "Ces dernières 24 heures"
- last_week: "La semaine dernière"
- last_month: "Le mois dernier"
- custom: "Personnalisé…"
- ai_persona:
- ai_tools: "Outils"
- tool_strategies:
- all: "Appliquer à toutes les réponses"
- replies:
- one: "Appliquer à la première réponse uniquement"
- other: "Appliquer aux %{count} premières réponses"
- back: "Retour"
- name: "Nom"
- edit: "Modifier"
- export: "Exporter"
- description: "Description"
- no_llm_selected: "Aucun modèle linguistique sélectionné"
- max_context_posts: "Nombre maximal de publications contextuelles"
- max_context_posts_help: "Le nombre maximal de publications à utiliser comme contexte pour l'IA lorsqu'elle répond à un utilisateur. (vide par défaut)"
- vision_enabled: Vision activée
- vision_enabled_help: Si cette option est activée, l'IA tentera de comprendre les images publiées par les utilisateurs dans le sujet, en fonction du modèle utilisé prenant en charge la vision. Pris en charge par les derniers modèles d'Anthropic, Google et OpenAI.
- vision_max_pixels: Taille d'image prise en charge
- vision_max_pixel_sizes:
- low: Qualité faible - moins cher (256x256)
- medium: Qualité moyenne (512x512)
- high: Qualité élevée - plus lent (1024x1024)
- tool_details: Afficher les détails de l'outil
- tool_details_help: Affiche aux utilisateurs finaux des informations sur les outils que le modèle linguistique a déclenchés.
- mentionable: Autoriser les mentions
- mentionable_help: Si cette option est activée, les utilisateurs des groupes autorisés peuvent mentionner cet utilisateur dans les publications, l'IA répondra en tant que personnage.
- user: Utilisateur
- create_user: Créer un utilisateur
- create_user_help: Vous pouvez éventuellement associer un utilisateur à ce personnage. Si vous le faites, l'IA utilisera cet utilisateur pour répondre aux demandes.
- default_llm: Modèle linguistique par défaut
- default_llm_help: Le modèle linguistique par défaut à utiliser pour ce personnage. Obligatoire si vous souhaitez mentionner un personnage sur des publications publiques.
- question_consolidator_llm: Modèle linguistique pour le consolidateur de questions
- question_consolidator_llm_help: Le modèle linguistique à utiliser pour le consolidateur de questions, vous pouvez choisir un modèle moins puissant pour réduire les coûts.
- system_prompt: Invite du système
- forced_tool_strategy: Stratégie d'outil forcé
- allow_chat_direct_messages: "Autoriser les messages directs de discussion"
- allow_chat_direct_messages_help: "Si cette option est activée, les utilisateurs des groupes autorisés peuvent envoyer des messages directs à ce personnage."
- allow_chat_channel_mentions: "Autoriser les mentions du canal de discussion"
- allow_chat_channel_mentions_help: "Si cette option est activée, les utilisateurs des groupes autorisés peuvent mentionner ce personnage dans les canaux de discussion."
- allow_personal_messages: "Autoriser les messages personnels"
- allow_personal_messages_help: "Si cette option est activée, les utilisateurs des groupes autorisés peuvent envoyer des messages personnels à ce personnage."
- allow_topic_mentions: "Autoriser les mentions de sujets"
- allow_topic_mentions_help: "Si cette option est activée, les utilisateurs des groupes autorisés peuvent mentionner ce personnage dans les sujets."
- force_default_llm: "Toujours utiliser le modèle linguistique par défaut"
- save: "Enregistrer"
- saved: "Personnage enregistré"
- enabled: "Activé ?"
- tools: "Outils activés"
- forced_tools: "Outils forcés"
- allowed_groups: "Groupes autorisés"
- confirm_delete: "Voulez-vous vraiment supprimer ce personnage ?"
- new: "Nouveau personnage"
- no_personas: "Vous n'avez pas encore créé de personnage"
- title: "Personnages"
- short_title: "Personnages"
- delete: "Supprimer"
- temperature: "Température"
- temperature_help: "Température à utiliser pour le LLM. Augmentez la valeur pour augmenter la créativité (laissez le champ vide pour utiliser la valeur par défaut du modèle, généralement une valeur comprise entre 0,0 et 2,0)"
- top_p: "Top P"
- top_p_help: "Top P à utiliser pour le LLM, augmentez pour augmenter le caractère aléatoire (laissez vide pour utiliser la valeur par défaut du modèle, généralement une valeur comprise entre 0,0 et 1,0)"
- priority: "Priorité"
- priority_help: "Les personnages prioritaires sont affichés aux utilisateurs en haut de la liste des personnages. Si plusieurs personnages sont prioritaires, ils seront triés par ordre alphabétique."
- tool_options: "Options de l'outil"
- rag_conversation_chunks: "Rechercher des morceaux de conversation"
- rag_conversation_chunks_help: "Le nombre de segments à utiliser pour les recherches de modèles RAG. Augmentez pour augmenter la quantité de contexte que l'IA peut utiliser."
- persona_description: "Les personnages sont une fonctionnalité puissante qui vous permet de personnaliser le comportement du moteur d'IA dans votre forum Discourse. Ils agissent comme un « message système » qui guide les réponses et les interactions de l'IA, en contribuant ainsi à créer une expérience utilisateur plus personnalisée et plus interactive."
- response_format:
- open_modal: "Modifier"
- modal:
- key_title: "Clé"
- filters:
- reset: "Réinitialiser"
- rag:
- options:
- rag_chunk_tokens: "Téléverser des jetons de morceaux"
- rag_chunk_tokens_help: "Le nombre de jetons à utiliser pour chaque morceau du modèle RAG. Augmentez la valeur pour augmenter la quantité de contexte que l'IA peut utiliser. (La modification indexera à nouveau tous les téléversements)"
- rag_chunk_overlap_tokens: "Téléverser des jetons de chevauchement de morceaux"
- rag_chunk_overlap_tokens_help: "Le nombre de jetons à superposer entre les morceaux dans le modèle RAG. (La modification indexera à nouveau tous les téléversements)"
- show_indexing_options: "Afficher les options de téléversement"
- hide_indexing_options: "Masquer les options de téléversement"
- uploads:
- title: "Fichiers envoyés"
- button: "Ajouter des fichiers"
- filter: "Filtrer les téléversements"
- indexed: "Indexé"
- indexing: "Indexation"
- uploaded: "Prêt à être indexé"
- uploading: "Envoi en cours…"
- remove: "Supprimer le téléversement"
- tools:
- back: "Retour"
- short_title: "Outils"
- export: "Exporter"
- no_tools: "Vous n'avez pas encore créé d'outil"
- name: "Nom"
- new: "Nouvel outil"
- description: "Description"
- description_help: "Une description claire de l'objectif de l'outil pour le modèle linguistique"
- subheader_description: "Les outils étendent les capacités des robots IA avec des fonctions JavaScript définies par l'utilisateur."
- summary: "Résumé"
- summary_help: "Résumé des outils destinés à être affichés aux utilisateurs finaux"
- script: "Script"
- parameters: "Paramètres"
- save: "Enregistrer"
- remove_parameter: "Supprimer"
- parameter_required: "Requis"
- parameter_enum: "Énumération"
- parameter_name: "Nom du paramètre"
- parameter_description: "Description du paramètre"
- enum_value: "Valeur d'énumération"
- add_enum_value: "Ajouter une valeur d'énumération"
- edit: "Modifier"
- test: "Lancer le test"
- delete: "Supprimer"
- saved: "Outil enregistré"
- confirm_delete: "Voulez-vous vraiment supprimer cet outil ?"
- test_modal:
- title: "Tester l'outil d'IA"
- run: "Lancer le test"
- result: "Résultat du test"
- llms:
- short_title: "LLM"
- no_llms: "Pas encore de LLM"
- new: "Nouveau modèle"
- display_name: "Nom"
- name: "ID du modèle"
- provider: "Fournisseur"
- tokenizer: "Tokéniseur"
- url: "URL du service hébergeant le modèle"
- api_key: "Clé API du service hébergeant le modèle"
- enabled_chat_bot: "Autoriser le sélecteur de robot IA"
- vision_enabled: "Vision activée"
- ai_bot_user: "Utilisateur robot IA"
- save: "Enregistrer"
- edit: "Modifier"
- saved: "Modèle LLM enregistré"
- back: "Retour"
- confirm_delete: Voulez-vous vraiment supprimer ce modèle ?
- delete: Supprimer
- seeded_warning: "Ce modèle est préconfiguré sur votre site et ne peut pas être modifié."
- quotas:
- title: "Quotas d'utilisation"
- add_title: "Créer un nouveau quota"
- group: "Groupe"
- max_tokens: "Nombre maximum de jetons"
- max_usages: "Nombre max. d'utilisations"
- duration: "Durée "
- confirm_delete: "Voulez-vous vraiment supprimer ce quota ?"
- add: "Ajouter un quota"
- durations:
- hour: "1 heure"
- six_hours: "6 heures"
- day: "24 heures"
- week: "7 jours"
- custom: "Personnalisé…"
- hours: "heures"
- max_tokens_help: "Nombre maximal de jetons (mots et caractères) que chaque utilisateur de ce groupe peut utiliser dans la durée spécifiée. Les jetons sont les unités utilisées par les modèles d'IA pour traiter le texte : environ 1 jeton = 4 caractères ou 3/4 d'un mot."
- max_usages_help: "Nombre maximal de fois que chaque utilisateur de ce groupe peut utiliser le modèle d'IA dans la durée spécifiée. Ce quota est suivi par utilisateur individuel et n'est pas partagé au sein du groupe."
- usage:
- ai_bot: "Robot IA"
- ai_helper: "Assistant"
- ai_persona: "Personnage (%{persona})"
- ai_summarization: "Résumer"
- ai_embeddings_semantic_search: "Recherche IA"
- ai_spam: "Spam"
- in_use_warning:
- one: "Ce modèle est actuellement utilisé par %{settings}. Si la configuration est incorrecte, la fonctionnalité ne fonctionnera pas comme prévu."
- other: "Ce modèle est actuellement utilisé par %{settings}. Si la configuration est incorrecte, les fonctionnalités ne fonctionneront pas comme prévu. "
- model_description:
- none: "Paramètres généraux qui fonctionnent pour la plupart des modèles linguistiques"
- anthropic-claude-opus-4-0: "Le modèle le plus intelligent d'Anthropic"
- anthropic-claude-3-5-haiku-latest: "Rapide et économique"
- google-gemini-2-5-flash: "Léger, rapide et économique avec raisonnement multimodal"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Modèle multilingue léger et efficace"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Modèle polyvalent et puissant"
- mistral-mistral-large-latest: "Le modèle le plus puissant de Mistral"
- mistral-pixtral-large-latest: "Le modèle de Mistral le plus performant en matière de vision"
- preseeded_model_description: "Modèle open source préconfiguré utilisant %{model}"
- configured:
- title: "LLM configurés"
- preconfigured_llms: "Sélectionnez votre LLM"
- preconfigured:
- title_no_llms: "Sélectionnez un modèle pour commencer"
- title: "Modèles LLM non configurés"
- description: "Les LLM (Large Language Models) sont des outils d'IA optimisés pour des tâches telles que la synthèse de contenu, la génération de rapports, l'automatisation des interactions avec les clients et la facilitation de la modération et des informations sur les forums"
- fake: "Configuration manuelle"
- button: "Configurer"
- next:
- title: "Suivant"
- tests:
- title: "Lancer le test"
- running: "Exécution du test..."
- success: "Succès !"
- failure: "La tentative de contact avec le modèle a renvoyé cette erreur : %{error}"
- hints:
- name: "Nous l'incluons dans l'appel d'API pour spécifier le modèle que nous allons utiliser"
- vision_enabled: "Si cette option est activée, l'IA tentera de comprendre les images. Cela dépend du modèle utilisé prenant en charge la vision. Pris en charge par les derniers modèles d'Anthropic, Google et OpenAI."
- enabled_chat_bot: "Si cette option est activée, les utilisateurs peuvent sélectionner ce modèle lors de la création de MD avec le robot IA"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Personnalisé"
- provider_fields:
- access_key_id: "ID de clé d'accès AWS Bedrock"
- region: "Région AWS Bedrock"
- organization: "ID d'organisation OpenAI facultatif"
- disable_system_prompt: "Désactiver le message système dans les invites"
- enable_native_tool: "Activer la prise en charge des outils natifs"
- disable_native_tools: "Désactiver la prise en charge des outils natifs (utiliser des outils basés sur XML)"
- provider_order: "Ordre des fournisseurs (liste délimitée par des virgules)"
- provider_quantizations: "Ordre de quantification des fournisseurs (liste délimitée par des virgules, par exemple : fp16,fp8)"
- disable_streaming: "Désactiver les complétions de streaming (convertir les requêtes de streaming en requêtes non-streaming)"
- related_topics:
- title: "Sujets connexes"
- pill: "Lié"
- ai_helper:
- title: "Suggérer des modifications à l'aide de l'IA"
- description: "Choisissez l'une des options ci-dessous et l'IA vous proposera une nouvelle version du texte."
- selection_hint: "Conseil : vous pouvez également sélectionner une partie du texte avant d'ouvrir l'assistant pour ne réécrire que cette partie."
- suggest: "Suggérer avec l'IA"
- suggest_errors:
- too_many_tags:
- one: "Vous ne pouvez avoir qu'un maximum de %{count} étiquette"
- other: "Vous ne pouvez avoir qu'un maximum de %{count} étiquettes"
- no_suggestions: "Aucune suggestion disponible"
- missing_content: "Veuillez saisir du contenu pour générer des suggestions."
- context_menu:
- trigger: "Demander à l'IA"
- loading: "L'IA est en train de générer"
- cancel: "Annuler"
- confirm: "Confirmer"
- discard: "Abandonner"
- changes: "Modifications suggérées"
- custom_prompt:
- title: "Invite personnalisée"
- placeholder: "Saisissez une invite personnalisée..."
- submit: "Envoyer une invite"
- translate_prompt: "Traduire en %{language}"
- post_options_menu:
- trigger: "Demander à l'IA"
- title: "Demander à l'IA"
- loading: "L'IA est en train de générer"
- close: "Fermer"
- copy: "Copier"
- copied: "Copié !"
- cancel: "Annuler"
- insert_footnote: "Ajouter une note de bas de page"
- footnote_disabled: "Insertion automatique désactivée. Cliquez sur le bouton Copier et modifiez-le manuellement"
- footnote_credits: "Explication par l'IA"
- fast_edit:
- suggest_button: "Suggérer une modification"
- thumbnail_suggestions:
- title: "Miniatures suggérées"
- select: "Sélectionner"
- selected: "Sélectionné"
- image_caption:
- button_label: "Légende avec IA"
- generating: "Génération de la légende..."
- credits: "Légende par l'IA"
- save_caption: "Enregistrer"
- automatic_caption_setting: "Activer le sous-titrage automatique"
- automatic_caption_loading: "Ajout des légendes des images..."
- automatic_caption_dialog:
- prompt: "Cette publication contient des images non légendées. Souhaitez-vous activer les légendes automatiques lors du téléversement d'images ? (Cela peut être modifié ultérieurement dans vos préférences)"
- confirm: "Activer"
- cancel: "Ne plus demander"
- no_content_error: "Ajoutez d'abord du contenu pour y effectuer des actions d'IA"
- reviewables:
- model_used: "Modèle utilisé :"
- accuracy: "Précision :"
- embeddings:
- short_title: "Intégrations"
- new: "Nouvelle intégration"
- back: "Retour"
- save: "Enregistrer"
- saved: "Configuration d'intégration enregistrée"
- delete: "Supprimer"
- confirm_delete: Voulez-vous vraiment supprimer cette configuration d'intégration ?
- empty: "Vous n'avez pas encore configuré les intégrations"
- presets: "Sélectionnez un préréglage..."
- configure_manually: "Configurer manuellement"
- edit: "Modifier"
- seeded_warning: "Cela est préconfiguré sur votre site et ne peut pas être modifié."
- tests:
- title: "Lancer le test"
- running: "Exécution du test..."
- success: "Succès !"
- failure: "La tentative de génération d'une intégration a donné le résultat suivant : %{error}"
- hints:
- dimensions_warning: "Une fois enregistrée, cette valeur ne peut plus être modifiée."
- matryoshka_dimensions: "Définit la taille des intégrations imbriquées utilisées pour la représentation hiérarchique ou multicouche des données, de la même manière que les poupées imbriquées s'emboîtent les unes dans les autres."
- sequence_length: "Le nombre maximal de jetons pouvant être traités simultanément lors de la création d'intégrations ou du traitement d'une requête."
- distance_function: "Détermine comment la similarité entre les intégrations est calculée, en utilisant soit la distance cosinus (mesure de l'angle entre les vecteurs) soit un produit interne négatif (mesure du chevauchement des valeurs vectorielles)."
- display_name: "Nom"
- provider: "Fournisseur"
- url: "URL du service d'intégration"
- api_key: "Clé d'API du service d'intégration"
- tokenizer: "Tokéniseur"
- dimensions: "Dimensions d'intégration"
- max_sequence_length: "Longueur de la séquence"
- embed_prompt: "Invite d'intégration"
- search_prompt: "Invite de recherche"
- matryoshka_dimensions: "Dimensions de la matriochka"
- distance_function: "Fonction de distance"
- distance_functions:
- "<#>": "Produit interne négatif"
- <=>: "Distance cosinus"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Personnalisé"
- provider_fields:
- model_name: "Nom du modèle"
- semantic_search: "Sujets (sémantiques)"
- semantic_search_loading: "Rechercher plus de résultats à l'aide de l'IA"
- semantic_search_results:
- toggle: "Affichage de %{count} résultats trouvés en utilisant l'IA"
- toggle_hidden: "Masquer %{count} résultats trouvés à l’aide de l'IA"
- none: "Nous sommes désolés, notre recherche par IA n'a trouvé aucun sujet correspondant"
- new: "Appuyez sur « Rechercher » pour commencer à rechercher de nouveaux résultats avec l'IA"
- unavailable: "Résultats de l'IA indisponibles"
- semantic_search_tooltips:
- results_explanation: "Lorsque cette option est activée, des résultats de recherche IA supplémentaires seront ajoutés ci-dessous."
- invalid_sort: "Les résultats de la recherche doivent être triés par pertinence pour afficher les résultats de l'IA"
- semantic_search_unavailable_tooltip: "Les résultats de la recherche doivent être triés par pertinence pour afficher les résultats de l'IA"
- ai_generated_result: "Résultat de recherche trouvé à l'aide de l'IA"
- quick_search:
- suffix: "dans tous les sujets et publications avec IA"
- ai_artifact:
- expand_view_label: "Agrandir la vue"
- collapse_view_label: "Quitter le mode plein écran (bouton ESC ou Retour)"
- click_to_run_label: "Exécuter l'artefact"
- ai_bot:
- llm: "Modèle"
- pm_warning: "Les messages du chatbot IA sont surveillés régulièrement par les modérateurs."
- cancel_streaming: "Arrêter de répondre"
- default_pm_prefix: "[Message privé de robot IA sans titre]"
- shortcut_title: "Démarrer une conversation avec un robot IA"
- share: "Copier la conversation avec l'IA"
- conversation_shared: "Conversation copiée"
- debug_ai: "Afficher la demande et la réponse brutes de l'IA"
- debug_ai_modal:
- title: "Afficher l'interaction avec l'IA"
- copy_request: "Copier la demande"
- copy_response: "Copier la réponse"
- request_tokens: "Jetons de demande :"
- response_tokens: "Jetons de réponse :"
- request: "Requête"
- response: "Réponse"
- next_log: "Suivant"
- previous_log: "Précédent"
- share_full_topic_modal:
- title: "Partager la conversation publiquement"
- share: "Partager et copier le lien"
- update: "Mettre à jour et copier le lien"
- delete: "Supprimer le partage"
- share_ai_conversation:
- name: "Partager une conversation IA"
- title: "Partager publiquement cette conversation avec l'IA"
- invite_ai_conversation:
- button: "Inviter"
- ai_label: "IA"
- ai_title: "Conversation avec l'IA"
- share_modal:
- title: "Copier la conversation avec l'IA"
- copy: "Copie"
- context: "Interactions à partager :"
- share_tip: "Vous pouvez également partager l'intégralité de la conversation"
- bot_names:
- fake: "Faux robot de test"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "Aujourd'hui"
- last_7_days: "Ces 7 derniers jours"
- last_30_days: "Ces 30 derniers jours"
- sentiments:
- dashboard:
- title: "Sentiment"
- sentiment_analysis:
- filter_types:
- all: "Tout"
- positive: "Positif"
- neutral: "Neutre"
- negative: "Négatif"
- group_types:
- category: "Catégorie"
- tag: "Étiqueter"
- table:
- sentiment: "Sentiment"
- total_count: "Total"
- summarization:
- chat:
- title: "Résumer les messages"
- description: "Sélectionnez une option ci-dessous pour résumer la conversation envoyée pendant la période souhaitée."
- summarize: "Résumer"
- since:
- one: "Dernière heure"
- other: "%{count} dernières heures"
- topic:
- title: "Résumé du sujet"
- close: "Fermer le panneau du résumé"
- topic_list_layout:
- button:
- compact: "Compact"
- expanded: "Étendu"
- expanded_description: "avec des résumés d'IA"
- discobot_discoveries:
- regular_results: "Sujets"
- collapse: "Réduire"
- tooltip:
- actions:
- disable: "Désactiver"
- review:
- types:
- reviewable_ai_post:
- title: "Publication signalée par l'IA"
- reviewable_ai_chat_message:
- title: "Message de conversation signalé par l'IA"
diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml
deleted file mode 100644
index 7aec9665..00000000
--- a/config/locales/client.gl.yml
+++ /dev/null
@@ -1,176 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-gl:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Ordenar por"
- tag:
- label: "Etiqueta"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID do tema"
- title:
- label: "Título"
- categories:
- label: "Categorías"
- tags:
- label: "Etiquetas"
- llm_triage:
- fields:
- category:
- label: "Categoría"
- tags:
- label: "Etiquetas"
- canned_reply:
- label: "Responder"
- discourse_ai:
- features:
- back: "Volver"
- disabled: "(desactivado)"
- groups: "Grupos:"
- no_groups: "Ningunha"
- edit: "Editar"
- expand_list:
- one: "(%{count} máis)"
- other: "(%{count} máis)"
- filters:
- all: "Todas"
- reset: "Restabelecer"
- search:
- name: "Buscar"
- spam:
- name: "Lixo"
- modals:
- select_option: "Seleccione unha opción..."
- spam:
- short_title: "Lixo"
- enable: "Activar"
- test_modal:
- spam: "Lixo"
- usage:
- summary: "Resumo"
- username: "Nome de usuario"
- total_requests: "Solicitudes totais"
- periods:
- custom: "Personalizar..."
- ai_persona:
- back: "Volver"
- name: "Nome"
- edit: "Editar"
- export: "Exportar"
- description: "Descrición"
- user: Usuario
- save: "Gardar"
- enabled: "Activado?"
- allowed_groups: "Grupos permitidos"
- delete: "Eliminar"
- response_format:
- open_modal: "Editar"
- modal:
- key_title: "Chave"
- filters:
- reset: "Restabelecer"
- rag:
- uploads:
- title: "Cargas"
- uploading: "Cargando..."
- tools:
- back: "Volver"
- export: "Exportar"
- name: "Nome"
- description: "Descrición"
- summary: "Resumo"
- save: "Gardar"
- remove_parameter: "Retirar"
- parameter_required: "Obrigatorio"
- edit: "Editar"
- delete: "Eliminar"
- llms:
- display_name: "Nome"
- save: "Gardar"
- edit: "Editar"
- back: "Volver"
- delete: Eliminar
- quotas:
- group: "Grupo"
- duration: "Duración"
- durations:
- hour: "1 hora"
- six_hours: "6 horas"
- day: "24 horas"
- custom: "Personalizar..."
- hours: "horas"
- usage:
- ai_spam: "Lixo"
- next:
- title: "Seguinte"
- tests:
- success: "Feito!"
- providers:
- google: "Google"
- fake: "Personalizado"
- ai_helper:
- context_menu:
- cancel: "Cancelar"
- confirm: "Confirmar"
- discard: "Desbotar"
- post_options_menu:
- close: "Pechar"
- copy: "Copiar"
- copied: "Copiado!"
- cancel: "Cancelar"
- image_caption:
- save_caption: "Gardar"
- automatic_caption_dialog:
- confirm: "Activar"
- embeddings:
- back: "Volver"
- save: "Gardar"
- delete: "Eliminar"
- edit: "Editar"
- tests:
- success: "Feito!"
- display_name: "Nome"
- providers:
- google: "Google"
- fake: "Personalizado"
- ai_bot:
- debug_ai_modal:
- request: "Petición"
- response: "Resposta"
- next_log: "Seguinte"
- previous_log: "Anterior"
- invite_ai_conversation:
- button: "Convidar"
- share_modal:
- copy: "Copiar"
- conversations:
- today: "Hoxe"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Todas"
- neutral: "Neutro"
- group_types:
- category: "Categoría"
- tag: "Etiqueta"
- table:
- total_count: "Total"
- discobot_discoveries:
- regular_results: "Temas"
- collapse: "Pregar"
- tooltip:
- actions:
- disable: "Desactivar"
diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml
deleted file mode 100644
index fe95e593..00000000
--- a/config/locales/client.he.yml
+++ /dev/null
@@ -1,831 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-he:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "מאפשר חיפוש בינה מלאכותית"
- stream_completion: "מאפשר הזרמת השלמות דמות בינה מלאכותית"
- update_personas: "מאפשר לעדכן דמויות בינה מלאכותית"
- site_settings:
- categories:
- discourse_ai: "בינה מלאכותית ב־Discourse"
- dashboard:
- emotion:
- title: "רגש"
- description: "הטבלה מפרטת את כמות הפוסטים שסווגו עם רגש שנקבע. הסיווג נעשה בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- reports:
- filters:
- group_by:
- label: "קיבוץ לפי"
- sort_by:
- label: "מיון על פי"
- tag:
- label: "תגית"
- logs:
- staff_actions:
- actions:
- create_ai_llm_model: "יצירת מודל LLM"
- update_ai_llm_model: "עדכון מודל LLM"
- delete_ai_llm_model: "מחיקת מודל LLM"
- create_ai_persona: "יצירת דמות בינה מלאכותית"
- update_ai_persona: "עדכון דמות בינה מלאכותית"
- delete_ai_persona: "מחיקת דמות בינה מלאכותית"
- create_ai_tool: "יצירת כלי בינה מלאכותית"
- update_ai_tool: "עדכון כלי בינה מלאכותית"
- delete_ai_tool: "מחיקת כלי בינה מלאכותית"
- create_ai_embedding: "יצירת הטמעת בינה מלאכותית"
- update_ai_embedding: "עדכון הטמעת בינה מלאכותית"
- delete_ai_embedding: "מחיקת הטמעת בינה מלאכותית"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "מוען"
- description: "המשתמש שישלח את הדוח"
- receivers:
- label: "נמענים"
- description: "המשתמשים שיקבלו את הדוח (כתובות דוא״ל - יועבר לדוא״ל ישירות, שמות משתמשים - הודעות פרטיות במערכת)"
- topic_id:
- label: "מזהה נושא"
- description: "מזהה הנושא לפרסם אליו את הדוח"
- title:
- label: "כותרת"
- description: "כותרת הדוח"
- days:
- label: "ימים"
- description: "פרק הזמן שמכסה הדוח"
- offset:
- label: "היסט"
- description: "בזמן בדיקה כדאי להריץ את הדוח היסטורית, יש להשתמש בהיסט כדי להתחיל את הדוח בתאריך מוקדם יותר"
- instructions:
- label: "הנחיות"
- description: "ההנחיות שסופקו לדגם השפה הגדול"
- sample_size:
- label: "גודל דגימה"
- description: "מספר הפוסטים לדגימה לדוח"
- tokens_per_post:
- label: "אסימונים לפוסט"
- description: "מספר אסימוני ה־LLM לשימוש לפוסט"
- model:
- label: "דגם"
- description: "LLM לשימוש להפקת דוחות"
- categories:
- label: "קטגוריות"
- description: "סינון הנושאים לקטגוריות האלו בלבד"
- tags:
- label: "תגיות"
- description: "סינון נושאים רק לתגיות האלה"
- exclude_tags:
- label: "החרגת תגיות"
- description: "החרגת נושאים עם התגיות האלו"
- exclude_categories:
- label: "החרגת קטגוריות"
- description: "החרגת נושאים עם הקטגוריות האלו"
- allow_secure_categories:
- label: "לאפשר קטגוריות מאובטחות"
- description: "לאפשר את הפקת הדוח לנושאים בקטגוריות מאובטחות"
- suppress_notifications:
- label: "דחיית התראות"
- description: "לדחות התראות שהדוח יכול לייצר על ידי עיבוד התוכן. הגדרה זו תמפה מחדש אזכורים וקישורים פנימיים."
- debug_mode:
- label: "מצב ניפוי תקלות"
- description: "יש להפעיל מצב ניפוי תקלות כדי לצפות בקלט והפלט הגולמיים של ה־LLM"
- priority_group:
- label: "קבוצת עדיפות"
- description: "תיעדוף תוכן מהקבוצה הזאת בדוח"
- temperature:
- label: "טמפרטורה"
- description: "טמפרטורה לשימוש למודל השפה הגדול (LLM), הגדלה תגדיל את האקראיות (להשאיר ריק כדי להשתמש בברירת המחדל של הדגם)"
- top_p:
- label: "ה־P המובילים"
- description: "ה־P המוביל לשימוש למודל השפה הגדול (LLM), הגדלה תגדיל את האקראיות (להשאיר ריק כדי להשתמש בברירת המחדל של הדגם)"
- llm_tool_triage:
- fields:
- model:
- label: "מודל"
- tool:
- label: "כלי"
- llm_persona_triage:
- fields:
- persona:
- label: "דמות"
- silent_mode:
- label: "מצב שקט"
- llm_triage:
- fields:
- system_prompt:
- label: "בקשת מערכת"
- description: "הבקשה שתשמש למיון ראשוני, נא לוודא שהתשובה היא מילה אחת בה ניתן להשתמש כדי להקפיץ את הפקודה"
- max_post_tokens:
- label: "כמות אסימונים מרבית לפוסט"
- description: "מספר האסימונים המרבי לסריקה בעזרת מיון ראשוני עם LLM"
- stop_sequences:
- label: "עצירת רצפים"
- description: "הנחיית המודל לעצור יצירת אסימונים בהגעה לערכים האלה"
- search_for_text:
- label: "חיפוש טקסט"
- description: "אם הטקסט הבא מופיע בתגובה של ה־LLM (מודל שפה גדול), להחיל את הפעולות האלה"
- category:
- label: "קטגוריה"
- description: "קטגוריה להחלה על הנושא"
- tags:
- label: "תגיות"
- description: "תגיות להחלה על הנושא"
- canned_reply:
- label: "להגיב"
- description: "טקסט גולמי של התגובות המקוננות לפוסט בנושא"
- canned_reply_user:
- label: "משתמש תגובה"
- description: "שם המשתמש של המשתמש לפרסום התגובה המקוננת"
- hide_topic:
- label: "הסתרת נושא"
- description: "הפיכת הנושא למוסתר בפני הציבור אם מוקפץ"
- flag_type:
- label: "סוג סימון"
- description: "סוג הסימון להחלה על הפוסט (ספאם או לקדם לסקירה)"
- flag_post:
- label: "סימון פוסט"
- description: "סימון פוסט (או כספאם או לסקירה)"
- include_personal_messages:
- label: "כולל הודעות פרטיות"
- description: "לסרוק ולאמת הודעות אישיות"
- reply_persona:
- label: "דמות לתגובה"
- model:
- label: "מודל"
- description: "מודל שפה שמשמש למיון ראשוני"
- temperature:
- label: "טמפרטורה"
- description: "טמפרטורה לשימוש למודל השפה הגדול (LLM), הגדלה תגדיל את האקראיות (להשאיר ריק כדי להשתמש בברירת המחדל של הדגם)"
- discourse_ai:
- title: "בינה מלאכותית"
- features:
- short_title: "יכולות"
- description: "אלו יכולות בינה מלאכותית שזמינות למבקרים באתר שלך. אפשר להגדיר אותן להשתמש בדמויות וב־LLMים (מש״גים) מסוימים ואפשר לשלוט בגישה אליהן על ידי קבוצות."
- back: "חזרה"
- disabled: "(כבוי)"
- persona:
- one: "דמות:"
- two: "דמויות:"
- many: "דמויות:"
- other: "דמויות:"
- groups: "קבוצות:"
- llm:
- one: "LLM/מש״ג:"
- two: "LLMים:"
- many: "LLMים:"
- other: "LLMים:"
- no_llm: "לא נבחר מש״ג/LLM"
- no_persona: "לא מוגדר"
- no_groups: "ללא"
- edit: "עריכה"
- expand_list:
- one: "(עוד %{count})"
- two: "(עוד %{count})"
- many: "(עוד %{count})"
- other: "(עוד %{count})"
- collapse_list: "(להציג פחות)"
- bot:
- name: "בוט"
- description: "צ׳אט בוט שיכול לענות על שאלות ולסייע למשתמשים בהודעות אישיות, בפורום ובצ׳אט"
- filters:
- all: "הכול"
- text: "חיפוש יכולות, דמויות, LLMים/מש״גים או קבוצות…"
- reset: "איפוס"
- summarization:
- name: "סיכומים"
- description: "להנגיש את כפתור הסיכום שמאפשר למבקרים לסכם נושאים"
- topic_summaries: "תקצירי נושאים."
- search:
- name: "חיפוש"
- description: "שיפור חוויית החיפוש על ידי אספקת תשובות שנוצרו על ידי בינה מלאכותית לשאילתות"
- discoveries: "תגליות"
- embeddings:
- name: "הטמעות"
- discord:
- name: "שילוב מול Discord"
- description: "הוספת היכולת לחפש בערוצי Discord"
- search: "חיפוש ב־Discord"
- inference:
- generate_concepts: "הסקת תפיסות"
- ai_helper:
- name: "מסייע"
- proofread: הגהת הטקסט
- title_suggestions: "הצעת כותרות"
- explain: "הסבר"
- illustrate_post: "איור פוסט"
- smart_dates: "תאריכים חכמים"
- translate: "תרגום"
- markdown_tables: "יצירת טבלה ב־Markdown"
- custom_prompt: "בקשה מותאמת אישית"
- image_caption: "הוספת כותרות לתמונות"
- translation:
- description: "תרגום תוכן לשפות נתמכות"
- locale_detector: "מזהה שפה ומקום"
- post_raw_translator: "מתרגם פוסטים גולמי"
- topic_title_translator: "מתרגם כותרות נושאים"
- short_text_translator: "מתרגם טקסטים קצרים"
- spam:
- name: "ספאם"
- modals:
- select_option: "בחירת אפשרות…"
- layout:
- table: "טבלה"
- card: "כרטיס"
- spam:
- short_title: "ספאם"
- title: "הגדרת טיפול בספאם"
- select_llm: "בחירת LLM"
- custom_instructions: "הנחיות מותאמות אישית"
- last_seven_days: "7 הימים האחרונים"
- scanned_count: "פוסטים נסרקו"
- false_positives: "סימון שגוי"
- false_negatives: "ספאם שהוחמץ"
- spam_detected: "ספאם זוהה"
- custom_instructions_placeholder: "הנחיות נקודתיות לאתר עבור הבינה המלאכותית כדי לזהות ספאם בצורה יותר מדויקת"
- enable: "הפעלה"
- settings_saved: "הגדרות נשמרו"
- no_llms: "אין LLMים זמינים"
- test_button: "בדיקה…"
- save_button: "שמירת השינויים"
- test_modal:
- title: "בדיקת זיהוי ספאם"
- post_url_label: "כתובת או מזהה של הפוסט"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 או מזהה פוסט"
- result: "תוצאה"
- scan_log: "יומן סריקה"
- run: "הרצת בדיקה"
- spam: "ספאם"
- not_spam: "לא ספאם"
- errors:
- scan_not_admin:
- action: "תיקון"
- resolved: "השגיאה נפתרה!"
- usage:
- short_title: "שימוש"
- summary: "תקציר"
- total_tokens: "סך כל האסימונים"
- tokens_over_time: "סימונים לאורך זמן"
- features_breakdown: "שימוש לפי יכולת"
- feature: "יכולת"
- usage_count: "כמות שימושים"
- model: "מודל"
- models_breakdown: "שימוש לפי מודל"
- users_breakdown: "שימוש לפי משתמש"
- all_features: "כל היכולות"
- all_models: "כל המודלים"
- username: "שם משתמש"
- total_requests: "סך כל הבקשות"
- request_tokens: "אסימוני בקשה"
- response_tokens: "אסימוני תגובה"
- net_request_tokens: "אסימוני בקשה מהרשת"
- cached_tokens: "אסימונים שמורים במטמון"
- cached_request_tokens: "אסימוני בקשה במטמון"
- total_spending: "עלות משוערת"
- no_users: "לא נמצאו נתוני שימוש של משתמשים"
- no_models: "לא נמצאו נתוני שימוש במודל"
- no_features: "לא נמצאו נתוני שימוש ביכולת"
- subheader_description: "אסימונים הם היחידות הבסיסיות שבהם משתמשים LLMים כדי להבין וליצור טקסט, נתוני שימוש עשויים להשפיע על העלויות"
- stat_tooltips:
- total_requests: "כל הבקשות שהוגשו ל־LLMים דרך Discourse"
- total_tokens: "כל האסימונים שבהם נעשה שימוש בעת שליחת בקשות ל־LLM"
- request_tokens: "אסימונים שנעשה בהם שימוש כאשר ה־LLM מנסה להבין מה נאמר"
- response_tokens: "אסימונים בשימוש כשה־LLM מגיב לבקשה שלך"
- cached_tokens: "אסימוני בקשה שעובדו בעבר שה־LLM משתמש בהם כדי לייעל את הביצועים והעלות"
- periods:
- last_day: "24 השעות האחרונות"
- last_week: "בשבוע שעבר"
- last_month: "בחודש שעבר"
- custom: "התאמה אישית…"
- ai_persona:
- ai_tools: "כלים"
- tool_strategies:
- all: "החלה על כל התגובות"
- replies:
- one: "החלה על התגובה הראשונה בלבד"
- two: "החלה על שתי התגובות הראשונות בלבד"
- many: "החלה על %{count} התגובות הראשונות בלבד"
- other: "החלה על %{count} התגובות הראשונות בלבד"
- back: "חזרה"
- name: "שם"
- edit: "עריכה"
- export: "ייצוא"
- description: "תיאור"
- no_llm_selected: "לא נבחר דגם שפה"
- use_parent_llm: "להשתמש במודל שפה עם פיצול אישיות"
- max_context_posts: "כמות מרבית של פוסטים להקשר"
- max_context_posts_help: "הכמות המרבית של פוסטים לשימוש כהקשר לבינה המלאכותית בעת מענה למשתמש (ריק לברירת מחדל)"
- vision_enabled: ראייה מופעלת
- vision_enabled_help: אם האפשרות פעילה, הבינה מלאכותית תנסה להבין תמונות שהמשתמשים מפרסמים בנושא, כתלות בתמיכה של המודל בעיבוד תמונות. נתמך על ידי המודלים העדכניים ביותר מבית Anthropic, Google ו־OpenAI.
- vision_max_pixels: גודל התמונות הנתמכות
- vision_max_pixel_sizes:
- low: איכות נמוכה - הזול ביותר (256×256)
- medium: איכות בינונית (512×512)
- high: איכות גבוהה - האיטית ביותר (1024×1024)
- tool_details: הצגת פרטי כלי
- tool_details_help: יופיעו פרטי משתמשי קצה בנוגע לאילו כלים מודל השפה הוזנק.
- mentionable: לאפשר אזכורים
- mentionable_help: אם האפשרות פעולה, משתמשים בקבוצות המורדות יכולים לאזכר את המשתמש הזה בפוסטים, הבינה המלאכותית תגיב בתור הדמות הזאת.
- user: משתמש
- create_user: יצירת משתמש
- create_user_help: אפשר לצרף משתמש לדמות הזאת כרשות. אם האפשרות הזאת תסומן, הבינה המלאכותית תשתמש במשתמש הזה כדי להגיב לבקשות.
- default_llm: מודל שפה כברירת מחדל
- default_llm_help: דגם ברירת המחדל לשימוש לדמות הזאת. נחוץ כדי לאזכר דמות בפוסטים ציבוריים.
- question_consolidator_llm: מודל שפה למגבש השאלות
- question_consolidator_llm_help: מודל השפה לשימוש עבור מגבש השאלות, אפשר לבחור במודל פחות חזק כדי לחסוך בעלויות.
- system_prompt: בקשת מערכת
- forced_tool_strategy: אסטרטגיית כלי כפוי
- allow_chat_direct_messages: "לאפשר הודעות ישירות בצ׳אט"
- allow_chat_direct_messages_help: "אם האפשרות פעילה, משתמשים בקבוצות המורשות יכולים לשלוח הודעות ישירות לדמות הזאת."
- allow_chat_channel_mentions: "לאפשר אזכורים בערוצי צ׳אט"
- allow_chat_channel_mentions_help: "אם האפשרות פעילה, משתמשים בקבוצות המורשות יכולים לאזכר את הדמות הזאת בערוצי צ׳אט."
- allow_personal_messages: "לאפשר הודעות פרטיות"
- allow_personal_messages_help: "אם האפשרות פעילה, משתמשים בקבוצות המורשות יכולים לשלוח הודעות פרטיות לדמות הזאת."
- allow_topic_mentions: "לאפשר אזכורים בנושאים"
- allow_topic_mentions_help: "אם האפשרות פעילה, משתמשים בקבוצות המורשות יכולים לאזכר את הדמות הזאת בנושאים."
- force_default_llm: "תמיד להשתמש במודל השפה כברירת מחדל"
- save: "שמירה"
- saved: "הדמות נשמרה"
- enabled: "מאופשר?"
- tools: "כלים פעילים"
- forced_tools: "כלים נאכפים"
- allowed_groups: "קבוצות מורשות"
- confirm_delete: "למחוק את הדמות?"
- new: "דמות חדשה"
- no_personas: "לא יצרת דמויות עדיין"
- title: "דמויות"
- short_title: "דמויות"
- delete: "מחיקה"
- temperature: "טמפרטורה"
- temperature_help: "טמפרטורה לשימוש למודל השפה הגדול (LLM), הגדלה תגדיל את היצירתיות (אפשר להשאיר ריק לשימוש בברירת המחדל של הדגם, בדרך כלל זה ערך בין 0.0 לבין 2.0)"
- top_p: "ה־P המובילים"
- top_p_help: "ה־P המובילים לשימוש למודל השפה הגדול (LLM), הגדלה תגדיל את היצירתיות (אפשר להשאיר ריק לשימוש בברירת המחדל של הדגם, בדרך כלל זה ערך בין 0.0 לבין 1.0)"
- priority: "עדיפות"
- priority_help: "דמויות בעדיפות גבוהה מוצגות למשתמשים בראש רשימת הדמויות. אם מספר דמויות הן בעדיפות הן תסודרנה לפי האלפבית."
- tool_options: "אפשרויות כלי"
- rag_conversation_chunks: "חיפוש בחלקי הדיון"
- rag_conversation_chunks_help: "מספר הנתחים לשימוש לחיפושים עם מודל ה־RAG. הגדלה תגדיל את כמות ההקשר בו יכולה להשתמש הבינה המלאכותית."
- persona_description: "דמויות הן יכולות רבות עוצמה שמאפשר להתאים את התנהגות מנוע הבינה המלאכותית בפורום הדיסקורס שלך. הן מתנהגות כמו ‚הודעות מערכת’ שמנחות את תגובות והתנהלות הבינה המלאכותית, כדי לסייע ליצור חוויית משתמש מותאמת ומקרבת יותר."
- response_format:
- title: "תבנית תגובת JSON"
- no_format: "לא צוינה תבנית JSON"
- open_modal: "עריכה"
- modal:
- root_title: "מבנה תגובה"
- key_title: "מפתח"
- examples:
- title: דוגמאות
- new: דוגמה חדשה
- remove: מחיקת דוגמה
- collapsable_title: "דוגמה מס׳ %{number}"
- user: "הודעת משתמש"
- model: "תגובת המודל"
- list:
- enabled: "בוט בינה מלאכותית?"
- ai_bot:
- title: "אפשרויות בוט בינה מלאכותית"
- save_first: "אפשרויות בוט בינה מלאכותית נוספות תהיינה זמינות לאחר שמירת הדמות."
- filters:
- text: "איתור דמות"
- reset: "איפוס"
- no_results: "לא נמצאו דמויות שתואמות למסננים שלך."
- all_features: "כל תכונה שהיא"
- features_list:
- one: "תכונה:"
- two: "תכונות:"
- many: "תכונות:"
- other: "תכונות:"
- llms_list: "LLM/מש״ג:"
- rag:
- title: "RAG"
- options:
- rag_chunk_tokens: "העלאת אסימוני חלקים"
- rag_chunk_tokens_help: "מספר האסימונים לשימוש לכל נתח במודל ה־RAG. הגדלה תגדיל את כמות ההקשר בו יכולה להשתמש הבינה המלאכותית. (שינוי יסדר את כל ההעלאות במפתח מחדש)"
- rag_chunk_overlap_tokens: "העלאת אסימוני חפיפת חלקים"
- rag_chunk_overlap_tokens_help: "מספר האסימון לחפיפה בין נתחים במודל ה־RAG. (שינוי יסדר את כל ההעלאות במפתח מחדש)"
- show_indexing_options: "הצגת אפשרויות העלאה"
- hide_indexing_options: "הסתרת אפשרויות העלאה"
- uploads:
- title: "העלאות"
- description: "PDF (*.pdf), טקסט פשוט (.txt) או markdown (.md)"
- description_with_images: "טקסט פשוט (.txt), markdown (.md), PDF (.pdf) או תמונה (.png, .jpeg)"
- button: "הוספת קבצים"
- filter: "סינון העלאות"
- indexed: "סודר במפתח"
- indexing: "מסודר במפתח"
- uploaded: "מוכן לסידור במפתח"
- uploading: "בהליכי העלאה…"
- remove: "הסרת העלאה"
- tools:
- back: "חזרה"
- short_title: "כלים"
- export: "ייצוא"
- no_tools: "לא יצרת כלים עדיין"
- name: "שם"
- name_help: "השם יופיע בממשק המשתמש של Discourse והוא המזהה הקצר שבו יש להשתמש כדי למצוא את הכלי בהגדרות שונות, הוא צריך להיות ייחודי (חובה)"
- new: "כלי חדש"
- tool_name: "שם הכלי"
- tool_name_help: "שם הכלי מוצג למודל השפה הגדול. הוא לא ייחודי, אבל הוא ייחודי לכל דמות. (הדמות מאומתת בשמירה)"
- description: "תיאור"
- description_help: "תיאור ברור של מטרת הכלי למודל השפה"
- subheader_description: "כלים מרחיבים את היכולות של בוטים של בינה מלאכותית עם פונקציות JavaScript המוגדרות על ידי המשתמשים."
- summary: "תקציר"
- summary_help: "סיכום מטרת הכלים להצגה למשתמשי קצה"
- script: "סקריפט"
- parameters: "משתנים"
- save: "שמירה"
- parameter_type: "סוג משתנה"
- add_parameter: "הוספת משתנה"
- remove_parameter: "הסרה"
- parameter_required: "נדרש"
- parameter_enum: "מונה"
- parameter_name: "שם משתנה"
- parameter_description: "תיאור משתנה"
- enum_value: "ערך מונה"
- add_enum_value: "הוספת ערך מונה"
- edit: "עריכה"
- test: "הרצת בדיקה"
- delete: "מחיקה"
- saved: "הכלי נשמר"
- confirm_delete: "למחוק את הכלי הזה?"
- test_modal:
- title: "כלי בדיקת בינה מלאכותית"
- run: "הרצת בדיקה"
- result: "תוצאת הבדיקה"
- llms:
- short_title: "LLMים"
- no_llms: "אין LLMים עדיין"
- new: "מודל חדש"
- display_name: "שם"
- name: "מזהה מודל"
- provider: "ספק"
- tokenizer: "מפרק לאסימונים"
- max_prompt_tokens: "חלון הקשר"
- max_output_tokens: "כמות אסימוני פלט מרבית"
- url: "כתובת השירות שמארח את המודל"
- api_key: "מפתח ה־API של השירות שמארח את המודל"
- enabled_chat_bot: "לאפשר בורר בוטים של בינה מלאכותית"
- vision_enabled: "ראייה מופעלת"
- ai_bot_user: "משתמש בוט בינה מלאכותית"
- cost_input: "עלות קלט"
- cost_cached_input: "עלות קלט מאוחסן"
- cost_output: "עלות פלט"
- save: "שמירה"
- edit: "עריכה"
- saved: "מודל ה־LLM נשמר"
- back: "חזרה"
- confirm_delete: למחוק את המודל הזה?
- delete: מחיקה
- seeded_warning: "המודל הזה מוגדר מראש באתר שלך ואי אפשר לערוך אותו."
- quotas:
- title: "מכסות שימוש"
- add_title: "יצירת מכסה חדשה"
- group: "קבוצה"
- max_tokens: "כמות אסימונים מרבית"
- max_usages: "כמות שימושים מרבית"
- duration: "משך"
- confirm_delete: "למחוק את המכסה הזאת?"
- add: "הוספת מכסה"
- durations:
- hour: "שעה"
- six_hours: "6 שעות"
- day: "24 שעות"
- week: "7 ימים"
- custom: "התאמה אישית…"
- hours: "שעתיים"
- max_usages_required: "חובה להגדיר אם לא הוגדרה כמות אסימונים מרבית"
- usage:
- ai_bot: "בוט בינה מלאכותית"
- ai_helper: "מסייע"
- ai_helper_image_caption: "כותרת תמונה"
- ai_persona: "דמות (%{persona})"
- ai_summarization: "סיכום"
- ai_embeddings_semantic_search: "חיפוש בינה מלאכותית"
- ai_spam: "ספאם"
- automation: "אוטומציה (%{persona})"
- in_use_warning:
- one: "המודל הזה משמש את %{settings}. אם לא מוגדר כראוי, היכולת לא תעבוד כמצופה."
- two: "המודל הזה משמש את: %{settings}. אם ההגדרות שגויות היכולת לא תעבודנה כמצופה."
- many: "המודל הזה משמש את: %{settings}. אם ההגדרות שגויות היכולת לא תעבודנה כמצופה."
- other: "המודל הזה משמש את: %{settings}. אם ההגדרות שגויות היכולת לא תעבודנה כמצופה."
- model_description:
- none: "הגדרות כלליות שעובדות עבור רוב המודלים של השפות"
- anthropic-claude-opus-4-0: "המודל החכם ביותר של Anthropic"
- anthropic-claude-3-5-haiku-latest: "מהיר וחסכוני"
- google-gemini-2-5-flash: "קליל, מהיר וחסכוני עם עיבוד מרובה נדבכים"
- google-gemini-2-0-flash-lite: "מודל חסכוני ומהיר"
- open_ai-o3: "מודל ההיגיון החזק ביותר מבית OpenAI"
- open_ai-o4-mini: "מודל הגיון מתקדם וחסכוני"
- open_ai-gpt-4-1-nano: "מודל GPT-4.1 החסכוני והמהיר ביותר."
- samba_nova-Meta-Llama-3-1-8B-Instruct: "דגם רב־לשוני, קליל ויעיל"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "מודל רב־תכליתי חזק"
- mistral-mistral-large-latest: "המודל החזק ביותר של Mistral"
- mistral-pixtral-large-latest: "המודל החזק ביותר של Mistral שבנוי לעיבוד חזותי"
- preseeded_model_description: "מודל קוד פתוח שהוגדר מראש ומשתמש ב־%{model}"
- configured:
- title: "LLMs (מודלי שפה גדולים) מוגדרים"
- preconfigured_llms: "בחירת ה־LLM שלך"
- preconfigured:
- title_no_llms: "נא לבחור תבנית כדי להתחיל"
- title: "תבניות LLM (מודלי שפה גדולים) לא מוגדרות"
- description: "מש״ג/LLM (מודלי שפה גדולים/Large Language Models) הם כלי בינה מלאכותית למשימות כגון סיכום תוכן, יצירת דוחות, אוטומציה של תקשורת מול לקוחות ופיקוח על הפורום והפקת תובנות ממנו"
- fake: "הגדרות ידניות"
- button: "הגדרה"
- next:
- title: "הבא"
- tests:
- title: "הרצת בדיקה"
- running: "הבדיקה רצה…"
- success: "הצליח!"
- failure: "הניסיון ליצור קשר עם המודל החזיר את השגיאה הבאה: %{error}"
- hints:
- display_name: "השם משמש להתייחסות למודל הזה על פני כלל ממשק האתר שלך."
- name: "אנו מצרפים את זה לקריאת ה־API כדי לציין באיזה מודל להשתמש"
- vision_enabled: "אם האפשרות פעילה, הבינה מלאכותית תנסה להבין תמונות. כתלות בתמיכה של המודל בעיבוד תמונות. נתמך על ידי המודלים העדכניים ביותר מבית Anthropic, Google ו־OpenAI."
- enabled_chat_bot: "אם האפשרות פעילה, משתמשים יכולים לבחור את המודל הזה בעת יצירת הודעות פרטיות עם בוט הבינה המלאכותית"
- cost_input: "עלות הקלט לכל מיליון אסימונים למודל זה"
- cost_cached_input: "עלות הקלט המאוחסן לכל מיליון אסימונים למודל זה"
- cost_output: "עלות הפלט לכל מיליון אסימונים למודל זה"
- cost_measure: "$/מיליון אסימונים"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "מותאם"
- provider_fields:
- access_key_id: "מזהה מפתח גישה ל־AWS Bedrock"
- region: "אזור של AWS Bedrock"
- organization: "מזהה ארגון OpenAI כרשות"
- disable_system_prompt: "השבתת הודעת מערכת בבקשות"
- enable_native_tool: "הפעלת תמיכה בכלים מובנים"
- disable_native_tools: "השבתת תמיכה בבלי מובנה (להשתמש בכלים מבוססי XML)"
- provider_order: "סדר ספקים (רשימה מופרדת בפסיקים)"
- provider_quantizations: "סדר כימות הספקים (רשימה מופרדת בפסיקים, למשל: fp16,fp8)"
- reasoning_effort: "מאמץ נימוק (תקף רק על מודלים של נימוק)"
- enable_reasoning: "הפעלת נימוק (תקף רק על מודלים עם אפשרות נימוק)"
- enable_thinking: "הפעלת חשיבה (רק במודלים תקפים לדוגמה: flash 2.5)"
- reasoning_tokens: "כמות אסימונים המשמשים לנימוק"
- related_topics:
- title: "נושאים קשורים"
- pill: "קשורים"
- ai_helper:
- title: "הצעת שינויים באמצעות בינה מלאכותית"
- description: "נא לבחור באחת מהאפשרויות להלן והבינה המלאכותית תציע לך גרסה חדשה של הטקסט."
- selection_hint: "עצה: אפשר לבחור חלק מהטקסט בטרם פתיחת המסייע כדי לשכתב רק את החלק הזה."
- suggest: "הצעה עם בינה מלאכותית"
- suggest_errors:
- too_many_tags:
- one: "יכולה להיות לך עד תגית אחת"
- two: "יכולות להיות לך עד שתי תגיות"
- many: "יכולות להיות לך עד %{count} תגיות"
- other: "יכולות להיות לך עד %{count} תגיות"
- no_suggestions: "אין הצעות זמינות"
- missing_content: "נא למלא קצת תוכן כדי לחולל הצעות."
- context_menu:
- trigger: "לשאול בינה מלאכותית"
- loading: "הבינה המלאכותית מייצרת"
- cancel: "ביטול"
- regen: "ניסיון נוסף"
- confirm: "אישור"
- discard: "להשליך"
- changes: "עריכות מוצעות"
- custom_prompt:
- title: "בקשה מותאמת אישית"
- placeholder: "נא למלא בקשה מותאמת…"
- submit: "שליחת בקשה"
- translate_prompt: "תרגום ל%{language}"
- post_options_menu:
- trigger: "לשאול בינה מלאכותית"
- title: "לשאול בינה מלאכותית"
- loading: "הבינה המלאכותית נוצרת"
- close: "סגירה"
- copy: "העתקה"
- copied: "הועתק!"
- cancel: "ביטול"
- insert_footnote: "הוספת הערת שוליים"
- footnote_disabled: "הכנסה אוטומטית מושבתת, יש ללחוץ על כפתור ההעתקה ולערוך אותו ידנית"
- footnote_credits: "הסבר של בינה מלאכותית"
- fast_edit:
- suggest_button: "הצעת עריכה"
- thumbnail_suggestions:
- title: "תמונות ממוזערות מומלצות"
- select: "בחירה"
- selected: "נבחרים"
- image_caption:
- button_label: "מתן כותרת עם בינה מלאכותית"
- generating: "הכותרת מתחוללת…"
- credits: "הכותרת נוספה ע״י בינה מלאכותית"
- save_caption: "שמירה"
- automatic_caption_setting: "הפעלת כותרת אוטומטית"
- automatic_caption_loading: "נוספות כותרות לתמונות…"
- automatic_caption_dialog:
- prompt: "הפוסט מכיל תמונות בלי כותרת. להפעיל כותרות אוטומטיות לתמונות שנשלחות? (אפשר לשנות את זה בהעדפות שלך מאוחר יותר)"
- confirm: "הפעלה"
- cancel: "לא לשאול שוב"
- no_content_error: "יש להוסיף קודם תוכן כדי לבצע עליו פעולות בינה מלאכותית"
- reviewables:
- model_used: "דגם בשימוש:"
- accuracy: "דיוק:"
- embeddings:
- short_title: "הטמעות"
- description: "הטמעות הן ייצוגים מספריים של נתונים שלוכדים משמעות וקשר, הפעלת יכולות הבינה המלאכותית של Discourse מאפשרת יכולות כגון נושאים קשורים וחיפוש בינה מלאכותית כדי להבין ולחבר תוכן."
- new: "הטמעה חדשה"
- back: "חזרה"
- save: "שמירה"
- saved: "הגדרות ההטמעה נשמרו"
- delete: "מחיקה"
- confirm_delete: להסיר את הגדרות ההטמעה האלה?
- empty: "לא הגדרת הטמעות עדיין"
- presets: "בחירת ערכה…"
- configure_manually: "הגדרה ידנית"
- edit: "עריכה"
- seeded_warning: "מוגדר מראש באתר שלך ואי אפשר לערוך אותו."
- tests:
- title: "הרצת בדיקה"
- running: "הבדיקה רצה…"
- success: "הצליח!"
- failure: "הניסיון לייצר את ההטמעות גרם ל־: %{error}"
- hints:
- dimensions_warning: "לאחר השמירה, אי אפשר לשנות את הערך הזה."
- display_name: "שם"
- provider: "ספק"
- url: "כתובת שירות הטמעה"
- api_key: "מפתח API לשירות הטמעה"
- tokenizer: "מפרק לאסימונים"
- dimensions: "ממדי הטמעה"
- max_sequence_length: "אורך רצף"
- embed_prompt: "בקשת הטמעה"
- search_prompt: "בקשת חיפוש"
- matryoshka_dimensions: "ממדי מטריושקה"
- distance_function: "פונקציית מרחק"
- distance_functions:
- <=>: "מרחק קוסינוס"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "מותאם"
- provider_fields:
- model_name: "שם המודל"
- semantic_search: "נושאים (סמנטיקה)"
- semantic_search_loading: "מתבצע חיפוש אחר תוצאות נוספות עם בינה מלאכותית"
- semantic_search_results:
- toggle: "מוצגות %{count} תוצאות שנמצאו באמצעות בינה מלאכותית"
- toggle_hidden: "מוסתרות %{count} תוצאות שנמצאו באמצעות בינה מלאכותית"
- none: "חיפוש הבינה המלאכותית שלנו לא מצא נושאים, עמך הסליחה"
- new: "יש ללחוץ על ‚חיפוש’ כדי להתחיל לחפש תוצאות חדשות עם בינה מלאכותית"
- unavailable: "תוצאות בינה מלאכותית לא זמינות"
- semantic_search_tooltips:
- invalid_sort: "יש לסדר את תוצאות החיפוש לפי רלוונטיות להצגת תוצאות בינה מלאכותית"
- semantic_search_unavailable_tooltip: "יש לסדר את תוצאות החיפוש לפי רלוונטיות להצגת תוצאות בינה מלאכותית"
- ai_generated_result: "תוצאות חיפוש שנמצאו באמצעות בינה מלאכותית"
- quick_search:
- suffix: "בכל הנושאים והפוסטים עם בינה מלאכותית"
- ai_artifact:
- expand_view_label: "הרחבת תצוגה"
- collapse_view_label: "יציאה ממסך מלא (כפתורי ESC או Back)"
- click_to_run_label: "הרצת תוצר"
- ai_bot:
- persona: "דמות"
- llm: "מודל"
- pm_warning: "הודעות בוט שיח בינה מלאכותית לא נאכפות דרך קבע על ידי המפקחים."
- cancel_streaming: "עצירת התגובה"
- default_pm_prefix: "[הודעה פרטית של בינה מלאכותית ללא כותרת]"
- shortcut_title: "התחלת הודעה פרטית עם בוט בינה מלאכותית"
- share: "העתקת דיון עם בינה מלאכותית"
- conversation_shared: "הדיון הועתק"
- debug_ai: "הצגת בקשה ותגובה גולמיות לבינה המלאכותית"
- sidebar_empty: "היסטוריית השיחה עם הבוט תופיע כאן."
- debug_ai_modal:
- title: "הצגת התכתובת מול הבינה המלאכותית"
- copy_request: "העתקת הבקשה"
- copy_response: "העתקת תגובה"
- request_tokens: "אסימוני בקשה:"
- response_tokens: "אסימוני תגובה:"
- request: "בקשה"
- response: "תשובה"
- next_log: "הבא"
- previous_log: "הקודם"
- share_full_topic_modal:
- title: "שיתוף שיחה באופן ציבורי"
- share: "שיתוף והעתקת קישור"
- update: "עדכון והעתקת קישור"
- delete: "מחיקת שיתוף"
- share_ai_conversation:
- name: "שיתוף דיון עם בינה מלאכותית"
- title: "שיתוף שיחת בינה מלאכותית באופן ציבורי"
- invite_ai_conversation:
- button: "להזמין"
- title: "הזמנה לשיחה עם בינה מלאכותית"
- ai_label: "בינה מלאכותית"
- ai_title: "דיון עם בינה מלאכותית"
- share_modal:
- title: "העתקת דיון עם בינה מלאכותית"
- copy: "העתקה"
- context: "פעילויות לשיתוף:"
- share_tip: "לחלופין, אפשר לשתף את כל הדיון"
- bot_names:
- fake: "בוט בדיקות מזויף"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT 4 טורבו"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- header: "איך אפשר לעזור?"
- submit: "הגשת שאלה"
- disclaimer: "בינה מלאכותית מחוללת עלולה לשגות. כדאי לאמת פרטים חשובים."
- placeholder: "הגשת שאלה…"
- new: "שאלה חדשה"
- messages_sidebar_title: "שיחות"
- today: "היום"
- last_7_days: "7 הימים האחרונים"
- last_30_days: "30 הימים האחרונים"
- upload_files: "העלאת קבצים"
- sentiments:
- dashboard:
- title: "רגש"
- sidebar:
- overview: "סקירת הבעה"
- analysis: "ניתוח הבעות"
- sentiment_analysis:
- share_chart: "העתקת קישור לתרשים"
- filter_types:
- all: "הכול"
- positive: "חיובי"
- neutral: "נייטרלי"
- negative: "שלילי"
- group_types:
- category: "קטגוריה"
- tag: "תגית"
- table:
- sentiment: "רגש"
- total_count: "סה״כ"
- summarization:
- chat:
- title: "סיכום הודעות"
- description: "נא לבחור אפשרות להלן כדי לסכם את הדיון שנשלח בפרק הזמן המבוקש."
- summarize: "סיכום"
- since:
- one: "שעה אחרונה"
- two: "שעתיים אחרונות"
- many: "%{count} השעות האחרונות"
- other: "%{count} השעות האחרונות"
- topic:
- title: "תקציר הנושא"
- close: "סגירת חלונית תקציר"
- topic_list_layout:
- button:
- compact: "מצומצם"
- expanded: "מורחב"
- expanded_description: "עם סיכוי בינה מלאכותית"
- discobot_discoveries:
- regular_results: "נושאים"
- tell_me_more: "אשמח לדעת עוד…"
- continue_convo: "המשך בשיחה…"
- collapse: "צמצום"
- tooltip:
- header: "חיפוש מופעל בינה מלאכותית"
- content: "חיפוש בשפה טבעית מופעל על גבי %{model}"
- actions:
- info: "איך זה עובד?"
- disable: "השבתה"
- user_preferences:
- empty: "אין הגדרות תואמות זמינות בשלב זה"
- review:
- types:
- reviewable_ai_post:
- title: "פוסט שסומן על ידי בינה מלאכותית"
- reviewable_ai_chat_message:
- title: "הודעת צ׳אט שסומנה על ידי בינה מלאכותית"
diff --git a/config/locales/client.hr.yml b/config/locales/client.hr.yml
deleted file mode 100644
index 568550c5..00000000
--- a/config/locales/client.hr.yml
+++ /dev/null
@@ -1,196 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-hr:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Sortiraj po"
- tag:
- label: "Označiti"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID teme"
- title:
- label: "Naslov"
- categories:
- label: "Kategorije"
- tags:
- label: "Oznake"
- llm_triage:
- fields:
- category:
- label: "Kategorija"
- tags:
- label: "Oznake"
- canned_reply:
- label: "Odgovor"
- discourse_ai:
- features:
- back: "Natrag"
- disabled: "(onemogućeno)"
- groups: "Grupe:"
- no_persona: "Nije postavljeno"
- no_groups: "Ništa"
- edit: "Uredi"
- expand_list:
- one: "(%{count} još)"
- few: "(%{count} još)"
- other: "(%{count} još)"
- collapse_list: "(prikaži manje)"
- filters:
- all: "Sve"
- reset: "Resetirati"
- search:
- name: "Pretraživanje"
- spam:
- name: "Nepoželjno"
- modals:
- select_option: "Odaberi opciju..."
- spam:
- short_title: "Nepoželjno"
- last_seven_days: "Zadnjih 7 dana"
- enable: "Omogućiti"
- test_modal:
- spam: "Nepoželjno"
- usage:
- summary: "Sažetak"
- username: "Korisničko ime"
- total_requests: "Ukupno zahtjeva"
- periods:
- last_day: "Posljednja 24 sata"
- custom: "Prilagođeno..."
- ai_persona:
- back: "Natrag"
- name: "Ime"
- edit: "Uredi"
- export: "Izvoz"
- description: "Opis"
- user: Korisnik
- save: "Spremi"
- enabled: "Omogućeno?"
- allowed_groups: "Dopuštene grupe"
- delete: "Pobriši"
- response_format:
- open_modal: "Uredi"
- modal:
- key_title: "Ključ"
- filters:
- reset: "Resetirati"
- rag:
- uploads:
- title: "Prijenosi"
- uploading: "Učitavanje..."
- tools:
- back: "Natrag"
- export: "Izvoz"
- name: "Ime"
- description: "Opis"
- summary: "Sažetak"
- save: "Spremi"
- remove_parameter: "Ukloni"
- parameter_required: "Potrebno"
- edit: "Uredi"
- delete: "Pobriši"
- llms:
- display_name: "Ime"
- save: "Spremi"
- edit: "Uredi"
- back: "Natrag"
- delete: Pobriši
- quotas:
- group: "Grupa"
- max_usages: "Max korisnika"
- duration: "Trajanje"
- durations:
- hour: "1 sat"
- six_hours: "6 sata"
- day: "24 sata"
- week: "7 dana"
- custom: "Prilagođeno..."
- hours: "sata"
- usage:
- ai_summarization: "Rezimirati"
- ai_spam: "Nepoželjno"
- next:
- title: "Sljedeći"
- tests:
- success: "Uspjeh!"
- providers:
- google: "Google"
- fake: "Posebna"
- ai_helper:
- context_menu:
- cancel: "Odustani"
- confirm: "Potvrdi"
- discard: "Odbaci"
- post_options_menu:
- close: "Zatvori"
- copy: "Kopija"
- copied: "Kopirano!"
- cancel: "Odustani"
- thumbnail_suggestions:
- select: "Odaberite"
- image_caption:
- save_caption: "Spremi"
- automatic_caption_dialog:
- confirm: "Omogućiti"
- embeddings:
- back: "Natrag"
- save: "Spremi"
- delete: "Pobriši"
- edit: "Uredi"
- tests:
- success: "Uspjeh!"
- display_name: "Ime"
- providers:
- google: "Google"
- fake: "Posebna"
- ai_bot:
- debug_ai_modal:
- request: "Zahtjev"
- response: "Odgovor"
- next_log: "Sljedeći"
- previous_log: "Predhodno"
- invite_ai_conversation:
- button: "Pozovite"
- share_modal:
- copy: "Kopija"
- conversations:
- today: "Danas"
- last_7_days: "Zadnjih 7 dana"
- last_30_days: "Zadnjih 30 dana"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Sve"
- group_types:
- category: "Kategorija"
- tag: "Označiti"
- table:
- total_count: "Ukupno"
- summarization:
- chat:
- title: "Sažmi poruke"
- description: "Odaberite opciju u nastavku da biste saželi razgovor poslan tijekom željenog vremenskog okvira."
- summarize: "Rezimirati"
- since:
- one: "Posljednji sat"
- few: "Posljednja %{count} sata"
- other: "Posljednjih %{count} sati"
- discobot_discoveries:
- regular_results: "Tema"
- collapse: "Sakrij"
- tooltip:
- actions:
- disable: "Onemogući"
diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml
deleted file mode 100644
index faef4253..00000000
--- a/config/locales/client.hu.yml
+++ /dev/null
@@ -1,193 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-hu:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Rendezés"
- tag:
- label: "Címke"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Témaazonosító"
- title:
- label: "Cím"
- categories:
- label: "Kategóriák"
- tags:
- label: "Címkék"
- llm_triage:
- fields:
- category:
- label: "Kategória"
- tags:
- label: "Címkék"
- canned_reply:
- label: "Válasz"
- discourse_ai:
- features:
- back: "Vissza"
- disabled: "(kikapcsolt)"
- groups: "Csoportok:"
- no_persona: "Nincs beállítva"
- no_groups: "Egyik sem"
- edit: "Szerkesztés"
- expand_list:
- one: "(%{count} további)"
- other: "(%{count} további)"
- collapse_list: "(kevesebb megjelenítése)"
- filters:
- all: "Összes"
- reset: "Alaphelyzetbe állítás"
- search:
- name: "Keresés"
- spam:
- name: "Kéretlen tartalom"
- modals:
- select_option: "Válasszon egy lehetőséget…"
- spam:
- short_title: "Kéretlen tartalom"
- last_seven_days: "Elmúlt 7 nap"
- enable: "Engedélyezés"
- test_modal:
- result: "Eredmény"
- spam: "Kéretlen tartalom"
- usage:
- summary: "Összefoglaló"
- username: "Felhasználónév"
- total_requests: "Összes kérés"
- periods:
- last_day: "Elmúlt 24 óra"
- custom: "Egyéni…"
- ai_persona:
- back: "Vissza"
- name: "Név"
- edit: "Szerkesztés"
- export: "Exportálás"
- description: "Leírás"
- user: Felhasználó
- save: "Mentés"
- enabled: "Engedélyezve?"
- delete: "Törlés"
- response_format:
- open_modal: "Szerkesztés"
- modal:
- key_title: "Kulcs"
- filters:
- reset: "Alaphelyzetbe állítás"
- rag:
- uploads:
- title: "Feltöltések"
- uploading: "Feltöltés…"
- tools:
- back: "Vissza"
- export: "Exportálás"
- name: "Név"
- description: "Leírás"
- summary: "Összefoglaló"
- save: "Mentés"
- remove_parameter: "Eltávolítás"
- parameter_required: "Kötelező"
- edit: "Szerkesztés"
- delete: "Törlés"
- llms:
- display_name: "Név"
- save: "Mentés"
- edit: "Szerkesztés"
- back: "Vissza"
- delete: Törlés
- quotas:
- group: "Csoport"
- max_usages: "Maximális használat"
- duration: "Időtartam"
- durations:
- hour: "1 óra"
- six_hours: "6 óra"
- day: "24 óra"
- week: "7 nap"
- custom: "Egyéni…"
- hours: "óra"
- usage:
- ai_summarization: "Összefoglalás"
- ai_spam: "Kéretlen tartalom"
- next:
- title: "Tovább"
- tests:
- success: "Sikeres!"
- providers:
- google: "Google"
- fake: "Egyéni"
- related_topics:
- pill: "Kapcsolódó"
- ai_helper:
- context_menu:
- cancel: "Mégse"
- confirm: "Megerősítés"
- discard: "Elvetés"
- post_options_menu:
- close: "Bezárás"
- copy: "Másolás"
- copied: "Másolva!"
- cancel: "Mégse"
- thumbnail_suggestions:
- selected: "Kiválasztott"
- image_caption:
- save_caption: "Mentés"
- automatic_caption_dialog:
- confirm: "Engedélyezés"
- embeddings:
- back: "Vissza"
- save: "Mentés"
- delete: "Törlés"
- edit: "Szerkesztés"
- tests:
- title: "Teszt futtatása"
- success: "Sikeres!"
- display_name: "Név"
- providers:
- google: "Google"
- fake: "Egyéni"
- ai_bot:
- debug_ai_modal:
- request: "Kérés"
- response: "Válasz"
- next_log: "Tovább"
- previous_log: "Vissza"
- invite_ai_conversation:
- button: "Meghívás"
- share_modal:
- copy: "Másolás"
- conversations:
- today: "Ma"
- last_7_days: "Elmúlt 7 nap"
- last_30_days: "Elmúlt 30 nap"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Összes"
- neutral: "Semleges"
- group_types:
- category: "Kategória"
- tag: "Címke"
- table:
- total_count: "Összes"
- summarization:
- chat:
- summarize: "Összefoglalás"
- discobot_discoveries:
- regular_results: "Témák"
- collapse: "Összecsukás"
- tooltip:
- actions:
- disable: "Letiltás"
diff --git a/config/locales/client.hy.yml b/config/locales/client.hy.yml
deleted file mode 100644
index 44898284..00000000
--- a/config/locales/client.hy.yml
+++ /dev/null
@@ -1,169 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-hy:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Դասավորել ըստ"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Թեմայի ID"
- title:
- label: "Վերնագիր"
- categories:
- label: "Կատեգորիաներ"
- tags:
- label: "Թեգեր"
- llm_triage:
- fields:
- category:
- label: "Կատեգորիա"
- tags:
- label: "Թեգեր"
- canned_reply:
- label: "Պատասխանել"
- discourse_ai:
- features:
- back: "Ետ"
- disabled: "(անջատված)"
- groups: "Խմբեր:"
- no_groups: "Ոչ մի"
- edit: "Խմբագրել"
- expand_list:
- one: "(%{count} ավելի)"
- other: "(%{count} ավելի)"
- collapse_list: "(ցույց տալ ավելի քիչ)"
- filters:
- all: "Բոլորը"
- reset: "Զրոյացնել"
- search:
- name: "Որոնում"
- spam:
- name: "Սպամ"
- modals:
- select_option: "Ընտրել..."
- spam:
- short_title: "Սպամ"
- enable: "Միացնել"
- test_modal:
- spam: "Սպամ"
- usage:
- summary: "Ամփոփումը"
- username: "Օգտանուն"
- total_requests: "Ընդհանուր հարցումներ"
- ai_persona:
- back: "Ետ"
- name: "Անուն"
- edit: "Խմբագրել"
- export: "Արտահանել"
- description: "Նկարագրությունը"
- user: Օգտատեր
- save: "Պահպանել"
- enabled: "Միացվա՞ծ է:"
- delete: "Ջնջել"
- response_format:
- open_modal: "Խմբագրել"
- modal:
- key_title: "Բանալի"
- filters:
- reset: "Զրոյացնել"
- rag:
- uploads:
- title: "Վերբեռնումներ"
- uploading: "Վերբեռնում..."
- tools:
- back: "Ետ"
- export: "Արտահանել"
- name: "Անուն"
- description: "Նկարագրությունը"
- summary: "Ամփոփումը"
- save: "Պահպանել"
- remove_parameter: "Հեռացնել"
- parameter_required: "Պարտադիր"
- edit: "Խմբագրել"
- delete: "Ջնջել"
- llms:
- display_name: "Անուն"
- save: "Պահպանել"
- edit: "Խմբագրել"
- back: "Ետ"
- delete: Ջնջել
- quotas:
- group: "Խմբավորել"
- max_usages: "Առավելագույն օգտագործման քանակը"
- durations:
- hour: "1 ժամով"
- six_hours: "6 ժամով"
- day: "Վերջին 24 ժամվա"
- week: "Վերջին 7 օրվա"
- hours: "ժամ"
- usage:
- ai_spam: "Սպամ"
- next:
- title: "Հաջորդը"
- tests:
- success: "Հաջողություն!"
- providers:
- google: "Google"
- fake: "Մասնավոր"
- ai_helper:
- context_menu:
- cancel: "Չեղարկել"
- discard: "Չեղարկել"
- post_options_menu:
- close: "Փակել"
- copy: "Կրկնօրինակել"
- cancel: "Չեղարկել"
- image_caption:
- save_caption: "Պահպանել"
- automatic_caption_dialog:
- confirm: "Միացնել"
- embeddings:
- back: "Ետ"
- save: "Պահպանել"
- delete: "Ջնջել"
- edit: "Խմբագրել"
- tests:
- success: "Հաջողություն!"
- display_name: "Անուն"
- providers:
- google: "Google"
- fake: "Մասնավոր"
- ai_bot:
- debug_ai_modal:
- request: "Հարցում"
- response: "Արձագանք"
- next_log: "Հաջորդը"
- previous_log: "Նախորդը"
- invite_ai_conversation:
- button: "Հրավիրել"
- share_modal:
- copy: "Կրկնօրինակել"
- conversations:
- today: "Այսօրվա"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Բոլորը"
- neutral: "Նեյտրալ"
- group_types:
- category: "Կատեգորիա"
- table:
- total_count: "Ամբողջը"
- discobot_discoveries:
- regular_results: "Թեմաներ"
- collapse: "Կրճատել"
- tooltip:
- actions:
- disable: "Անջատել"
diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml
deleted file mode 100644
index 28e38759..00000000
--- a/config/locales/client.id.yml
+++ /dev/null
@@ -1,289 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-id:
- admin_js:
- admin:
- site_settings:
- categories:
- discourse_ai: "Discourse"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Pengirim"
- description: "Pengguna yang akan mengirim laporan"
- receivers:
- label: "Penerima"
- description: "Pengguna yang akan menerima laporan (email akan dikirim email langsung, nama pengguna akan dikirim PM)"
- topic_id:
- label: "ID Topik"
- title:
- label: "Judul"
- description: "Judul laporan"
- days:
- label: "Hari"
- description: "Rentang waktu laporan"
- offset:
- label: "Offset"
- description: "Saat menguji, Anda mungkin ingin mengoperasikan laporan secara historis, gunakan offset untuk memulai laporan pada tanggal yang lebih awal"
- instructions:
- label: "Instruksi"
- description: "Instruksi diberikan kepada model bahasa besar"
- sample_size:
- label: "Ukuran sampel"
- description: "Jumlah posting yang akan dijadikan sampel laporan"
- tokens_per_post:
- label: "Token per posting"
- model:
- label: "Model"
- description: "LLM yang akan digunakan untuk pembuatan laporan"
- categories:
- label: "Kategori"
- description: "Filter topik hanya ke kategori ini"
- tags:
- label: "Label"
- description: "Filter topik hanya ke tag ini"
- exclude_tags:
- label: "Kecualikan Tag"
- description: "Kecualikan topik dengan tag ini"
- exclude_categories:
- label: "Kecualikan Kategori"
- description: "Kecualikan topik dengan kategori ini"
- allow_secure_categories:
- label: "Izinkan kategori aman"
- description: "Izinkan laporan dibuat untuk topik dalam kategori aman"
- suppress_notifications:
- label: "Tiadakan Notifikasi"
- description: "Tiadakan notifikasi yang mungkin dihasilkan laporan dengan mengubahnya menjadi konten. Ini akan memetakan kembali sebutan dan tautan internal."
- debug_mode:
- label: "Mode Debug"
- description: "Aktifkan mode debug untuk melihat input dan output mentah LLM"
- priority_group:
- label: "Kelompok Prioritas"
- description: "Prioritaskan konten dari grup ini dalam laporan"
- temperature:
- label: "Temperatur"
- top_p:
- label: "Top P"
- llm_tool_triage:
- fields:
- model:
- label: "Model"
- llm_triage:
- fields:
- system_prompt:
- label: "Perintah Sistem"
- description: "Perintah yang akan digunakan untuk melakukan triase, pastikan untuk membalas dengan satu kata yang dapat Anda gunakan untuk memicu tindakan"
- search_for_text:
- label: "Cari teks"
- category:
- label: "Kategori"
- description: "Kategori untuk diterapkan pada topik"
- tags:
- label: "Label"
- canned_reply:
- label: "Balas"
- model:
- label: "Model"
- temperature:
- label: "Temperatur"
- discourse_ai:
- title: "AI"
- features:
- back: "Kembali"
- disabled: "(dinonaktifkan)"
- groups: "Grup:"
- no_groups: "Tidak ada"
- edit: "Ubah"
- expand_list:
- other: "(%{count} lainnya)"
- collapse_list: "(tampilkan lebih sedikit)"
- filters:
- all: "Semua"
- reset: "Reset"
- search:
- name: "Cari"
- spam:
- name: "Spam"
- modals:
- select_option: "Memilih sebuah pilihan..."
- spam:
- short_title: "Spam"
- last_seven_days: "7 hari terakhir"
- enable: "Aktifkan"
- test_modal:
- spam: "Spam"
- usage:
- summary: "Ringkasan"
- model: "Model"
- username: "Nama Pengguna"
- periods:
- last_day: "24 jam terakhir"
- ai_persona:
- back: "Kembali"
- name: "Nama"
- edit: "Ubah"
- export: "Ekspor"
- description: "Deskripsi"
- mentionable_help: Jika diaktifkan, pengguna di grup yang diizinkan dapat menyebut pengguna ini di posting, AI akan merespons sebagai persona ini.
- user: Pengguna
- question_consolidator_llm: Model Bahasa untuk Konsolidator Pertanyaan
- question_consolidator_llm_help: Model bahasa yang digunakan untuk konsolidator pertanyaan, Anda dapat memilih model yang kurang kuat untuk menghemat biaya.
- save: "Simpan"
- enabled: "Diaktifkan?"
- allowed_groups: "Grup yang diizinkan"
- delete: "Hapus"
- temperature: "Temperatur"
- top_p: "Top P"
- response_format:
- open_modal: "Ubah"
- filters:
- reset: "Reset"
- rag:
- uploads:
- uploading: "Mengunggah..."
- tools:
- back: "Kembali"
- export: "Ekspor"
- name: "Nama"
- description: "Deskripsi"
- summary: "Ringkasan"
- save: "Simpan"
- remove_parameter: "Hapus"
- edit: "Ubah"
- delete: "Hapus"
- llms:
- display_name: "Nama"
- save: "Simpan"
- edit: "Ubah"
- back: "Kembali"
- delete: Hapus
- quotas:
- duration: "Durasi"
- durations:
- six_hours: "6 jam"
- day: "24 jam"
- week: "7 hari"
- hours: "jam"
- usage:
- ai_summarization: "Meringkas"
- ai_spam: "Spam"
- tests:
- running: "Tes berlangsung..."
- success: "Sukses!"
- failure: "Mencoba menghubungi model menghasilkan kesalahan ini: %{error}"
- providers:
- google: "Google"
- ai_helper:
- missing_content: "Silakan masukkan beberapa konten untuk menghasilkan saran."
- context_menu:
- trigger: "Tanya AI"
- loading: "AI sedang menghasilkan"
- cancel: "Batal"
- confirm: "Konfirmasi"
- discard: "Batalkan"
- custom_prompt:
- placeholder: "Masukkan perintah khusus..."
- post_options_menu:
- trigger: "Tanya AI"
- title: "Tanya AI"
- loading: "AI sedang menghasilkan"
- close: "Tutup"
- copy: "Menyalin"
- copied: "Disalin!"
- cancel: "Batal"
- insert_footnote: "Tambahkan catatan kaki"
- footnote_credits: "Penjelasan oleh AI"
- thumbnail_suggestions:
- select: "Pilih"
- selected: "Dipilih"
- image_caption:
- button_label: "Keterangan dengan AI"
- generating: "Menghasilkan keterangan..."
- credits: "Keterangan oleh AI"
- save_caption: "Simpan"
- automatic_caption_dialog:
- confirm: "Aktifkan"
- reviewables:
- model_used: "Model yang digunakan:"
- accuracy: "Akurasi:"
- embeddings:
- back: "Kembali"
- save: "Simpan"
- delete: "Hapus"
- edit: "Ubah"
- tests:
- running: "Tes berlangsung..."
- success: "Sukses!"
- display_name: "Nama"
- providers:
- google: "Google"
- semantic_search: "Topik (Semantik)"
- semantic_search_loading: "Mencari hasil lebih banyak menggunakan AI"
- semantic_search_results:
- toggle: "Menampilkan %{count} hasil yang ditemukan menggunakan AI"
- toggle_hidden: "Menyembunyikan %{count} hasil yang ditemukan menggunakan AI"
- none: "Maaf, pencarian AI kami tidak menemukan topik yang cocok"
- ai_generated_result: "Hasil pencarian ditemukan menggunakan AI"
- quick_search:
- suffix: "di semua topik dan posting dengan AI"
- ai_bot:
- llm: "Model"
- pm_warning: "Pesan chatbot AI dipantau secara berkala oleh moderator."
- cancel_streaming: "Berhenti membalas"
- default_pm_prefix: "[PM bot AI tanpa judul]"
- shortcut_title: "Mulai PM dengan bot AI"
- share: "Salin percakapan AI"
- conversation_shared: "Percakapan disalin"
- debug_ai_modal:
- request: "Pinta"
- share_ai_conversation:
- name: "Bagikan percakapan AI"
- title: "Bagikan percakapan AI ini secara publik"
- invite_ai_conversation:
- button: "Undang"
- ai_label: "AI"
- ai_title: "Percakapan dengan AI"
- share_modal:
- title: "Salin percakapan AI"
- copy: "Salin"
- context: "Interaksi untuk dibagikan:"
- share_tip: "Alternatifnya, Anda dapat membagikan seluruh percakapan"
- bot_names:
- fake: "Bot Tes Palsu"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Soneta"
- claude-3-haiku: "Claude 3 Haiku"
- claude-2: "Claude 2"
- conversations:
- last_7_days: "7 hari terakhir"
- last_30_days: "30 hari terakhir"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Semua"
- group_types:
- category: "Kategori"
- table:
- total_count: "Total"
- summarization:
- chat:
- title: "Meringkas pesan"
- description: "Pilih opsi di bawah untuk meringkas percakapan yang dikirim selama jangka waktu yang diinginkan."
- summarize: "Meringkas"
- since:
- other: "%{count} jam terakhir"
- topic:
- title: "Ringkasan topik"
- discobot_discoveries:
- regular_results: "Topik"
- collapse: "Persempit"
- tooltip:
- actions:
- disable: "Nonaktifkan"
diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml
deleted file mode 100644
index 22caacb5..00000000
--- a/config/locales/client.it.yml
+++ /dev/null
@@ -1,686 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-it:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Consente la ricerca IA"
- stream_completion: "Consente lo streaming di completamenti di personaggi IA"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "Emozione"
- description: "La tabella elenca un conteggio di messaggi classificati con un'emozione determinata. Classificati con il modello 'SamLowe/roberta-base-go_emotions'."
- reports:
- filters:
- sort_by:
- label: "Ordina per"
- tag:
- label: "Etichetta"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Mittente"
- description: "L'utente che invierà il report"
- receivers:
- label: "Destinatari"
- description: "Gli utenti che riceveranno il report (le e-mail verranno inviate direttamente, i nomi utente verranno inviati un MP)"
- topic_id:
- label: "ID dell'argomento"
- description: "L'ID dell'argomento in cui pubblicare il report"
- title:
- label: "Titolo"
- description: "Il titolo del report"
- days:
- label: "Giorni"
- description: "L'arco temporale del report"
- offset:
- label: "Scostamento"
- description: "Durante il test, potresti voler eseguire il report in modo cronologico, utilizza lo scostamento per avviare il report in una data precedente"
- instructions:
- label: "Istruzioni"
- description: "Le istruzioni fornite al large language model"
- sample_size:
- label: "Dimensione del campione"
- description: "Il numero di messaggi da campionare per il report"
- tokens_per_post:
- label: "Token per messaggio"
- description: "Il numero di token LLM da utilizzare per messaggio"
- model:
- label: "Modello"
- description: "LLM da utilizzare per la generazione del report"
- categories:
- label: "Categorie"
- description: "Filtra gli argomenti solo in queste categorie"
- tags:
- label: "Etichette"
- description: "Filtra gli argomenti solo in base a queste etichette"
- exclude_tags:
- label: "Escludi tag"
- description: "Escludi argomenti con questi tag"
- exclude_categories:
- label: "Escludi categorie"
- description: "Escludi argomenti con queste categorie"
- allow_secure_categories:
- label: "Consenti categorie sicure"
- description: "Consenti la generazione del report per argomenti in categorie sicure"
- suppress_notifications:
- label: "Evita le notifiche"
- description: "Evita le notifiche che il report potrebbe generare trasformandosi in contenuto. Ciò rimapperà le menzioni e i link interni."
- debug_mode:
- label: "Modalità di debug"
- description: "Abilita la modalità di debug per visualizzare input e output non elaborati del LLM"
- priority_group:
- label: "Gruppo prioritario"
- description: "Dai la priorità ai contenuti di questo gruppo nel report"
- temperature:
- label: "Temperatura"
- top_p:
- label: "P superiore"
- llm_tool_triage:
- fields:
- model:
- label: "Modello"
- llm_triage:
- fields:
- system_prompt:
- label: "Comando di sistema"
- description: "Il comando che verrà utilizzato per il triage, assicurati che risponda con una sola parola che puoi usare per attivare l'azione"
- max_post_tokens:
- label: "Numero massimo di token di messaggio"
- description: "Numero massimo di token per scansionare utilizzando il triage LLM"
- stop_sequences:
- label: "Sequenze di arresto"
- description: "Indica al modello di interrompere la generazione del token quando si arriva a uno di questi valori"
- search_for_text:
- label: "Cerca testo"
- description: "Se nella risposta del LLM viene visualizzato il testo seguente, applica queste azioni"
- category:
- label: "Categoria"
- description: "Categoria da applicare all'argomento"
- tags:
- label: "Etichette"
- description: "Etichette da applicare all'argomento"
- canned_reply:
- label: "Rispondi"
- description: "Testo non elaborato della risposta predefinita al messaggio sull'argomento"
- canned_reply_user:
- label: "Utente della risposta"
- description: "Nome utente dell'utente che pubblicherà la risposta predefinita"
- hide_topic:
- label: "Nascondi argomento"
- description: "Rendi l'argomento non visibile al pubblico se attivato"
- flag_type:
- label: "Tipo di segnalazione"
- description: "Tipo di segnalazione da applicare al messaggio (spam o semplicemente passa in revisione)"
- flag_post:
- label: "Segnala messaggio"
- description: "Segnala il post (come spam o per la revisione)"
- include_personal_messages:
- label: "Includi messaggi personali"
- description: "Esegui anche la scansione e la selezione dei messaggi personali"
- model:
- label: "Modello"
- description: "Modello linguistico utilizzato per il triage"
- temperature:
- label: "Temperatura"
- discourse_ai:
- title: "IA"
- features:
- back: "Indietro"
- disabled: "(disabilitati)"
- groups: "Gruppi:"
- no_persona: "Non impostato"
- no_groups: "Nessuna"
- edit: "Modifica"
- expand_list:
- one: "(%{count} altri)"
- other: "(%{count} altri)"
- collapse_list: "(mostra meno)"
- filters:
- all: "Tutte"
- reset: "Annulla"
- search:
- name: "Cerca"
- embeddings:
- name: "Integrazioni"
- ai_helper:
- name: "Assistente"
- proofread: Testo corretto
- explain: "Spiega"
- smart_dates: "Date smart"
- markdown_tables: "Genera tabella di markdown"
- custom_prompt: "Comando personalizzato"
- spam:
- name: "Spam"
- description: "Identifica lo spam potenziale utilizzando l'LLM selezionato e lo segnala ai moderatori del sito affinché lo ispezionino nella coda di revisione"
- modals:
- select_option: "Scegli un'opzione..."
- spam:
- short_title: "Spam"
- title: "Configura la gestione dello spam"
- select_llm: "Seleziona LLM"
- custom_instructions: "Istruzioni personalizzate"
- custom_instructions_help: "Istruzioni personalizzate specifiche per il tuo sito per aiutare l'intelligenza artificiale a identificare lo spam, ad esempio \"Sii più aggressivo nell'analizzare i post in una lingua diversa dall'italiano\"."
- last_seven_days: "Ultimi 7 giorni"
- scanned_count: "Messaggi scansionati"
- false_positives: "Segnalato in modo errato"
- false_negatives: "Spam mancato"
- spam_detected: "Spam rilevato"
- custom_instructions_placeholder: "Istruzioni specifiche del sito per l'IA per aiutare a identificare lo spam in modo più accurato"
- enable: "Abilita"
- spam_tip: "Il rilevamento IA dello spam analizzerà i primi 3 messaggi di tutti i nuovi utenti su argomenti pubblici. Li contrassegnerà per la revisione e bloccherà gli utenti se rappresentano verosimilmente spam."
- settings_saved: "Impostazioni salvate"
- spam_description: "Identifica lo spam potenziale utilizzando l'LLM selezionato e lo segnala ai moderatori del sito affinché lo ispezionino nella coda di revisione"
- no_llms: "Nessun LLM disponibile"
- test_button: "Test..."
- save_button: "Salva le modifiche"
- test_modal:
- title: "Prova il rilevamento dello spam"
- post_url_label: "URL o ID del messaggio"
- post_url_placeholder: "https://tuo-forum.com/t/topic/123/4 oppure l'ID del messaggio"
- result: "Risultato"
- scan_log: "Registro di scansione"
- run: "Esegui test"
- spam: "Spam"
- not_spam: "Non è spam"
- stat_tooltips:
- incorrectly_flagged: "Elementi che il bot IA ha contrassegnato come spam su cui i moderatori non erano d'accordo"
- missed_spam: "Elementi segnalati dalla community come spam che non sono stati rilevati dal bot IA, con cui i moderatori hanno concordato"
- errors:
- scan_not_admin:
- message: "Attenzione: la scansione antispam non funzionerà correttamente perché l'account di scansione antispam non è un amministratore"
- action: "Correggi"
- resolved: "L'errore è stato risolto!"
- usage:
- short_title: "Utilizzo"
- summary: "Riepilogo"
- total_tokens: "Totale token"
- tokens_over_time: "Token nel tempo"
- features_breakdown: "Utilizzo per funzionalità"
- feature: "Funzionalità"
- usage_count: "Conteggio utilizzo"
- model: "Modello"
- models_breakdown: "Utilizzo per modello"
- users_breakdown: "Utilizzo per utente"
- all_features: "Tutte le funzionalità"
- all_models: "Tutti i modelli"
- username: "Nome utente"
- total_requests: "Richieste totali"
- request_tokens: "Token di richiesta"
- response_tokens: "Token di risposta"
- net_request_tokens: "Token di richiesta netti"
- cached_tokens: "Token in cache"
- cached_request_tokens: "Token di richiesta memorizzati nella cache"
- no_users: "Nessun dato trovato sull'utilizzo dell'utente"
- no_models: "Nessun dato trovato sull'utilizzo del modello"
- no_features: "Nessun dato trovato sull'utilizzo delle funzionalità"
- subheader_description: "I token sono le unità di base che gli LLM utilizzano per comprendere e generare testo; i dati di utilizzo possono incidere sui costi"
- stat_tooltips:
- total_requests: "Tutte le richieste inoltrate agli LLM tramite Discourse"
- total_tokens: "Tutti i token utilizzati quando si richiede un LLM"
- request_tokens: "Token utilizzati quando l'LLM cerca di capire cosa stai dicendo"
- response_tokens: "Token utilizzati quando l'LLM risponde al tuo comando"
- cached_tokens: "Token di richiesta elaborati in precedenza che LLM riutilizza per ottimizzare prestazioni e costi"
- periods:
- last_day: "Ultime 24 ore"
- last_week: "Ultima settimana"
- last_month: "Ultimo mese"
- custom: "Personalizza..."
- ai_persona:
- ai_tools: "Strumenti"
- tool_strategies:
- all: "Applica a tutte le risposte"
- replies:
- one: "Applica solo alla prima risposta"
- other: "Applica alle prime %{count} risposte"
- back: "Indietro"
- name: "Nome"
- edit: "Modifica"
- export: "Esporta"
- description: "Descrizione"
- no_llm_selected: "Nessun modello linguistico selezionato"
- max_context_posts: "Numero massimo di messaggi di contesto"
- max_context_posts_help: "Il numero massimo di post da utilizzare come contesto per l'IA quando si risponde a un utente. (vuoto per impostazione predefinita)"
- vision_enabled: Visione abilitata
- vision_enabled_help: Se l'opzione è abilitata, l'intelligenza artificiale tenterà di comprendere le immagini che gli utenti pubblicano nell'argomento, a seconda del modello utilizzato per supportare la visione. Supportato dagli ultimi modelli di Anthropic, Google e OpenAI.
- vision_max_pixels: Dimensione immagine supportata
- vision_max_pixel_sizes:
- low: 'Bassa qualità: più veloce (256x256)'
- medium: Qualità media (512x512)
- high: 'Alta qualità: più lenta (1024x1024)'
- tool_details: Mostra i dettagli dello strumento
- tool_details_help: Mostrerà agli utenti finali i dettagli su quali strumenti ha attivato il modello linguistico.
- mentionable: Consenti menzioni
- mentionable_help: Se l'opzione è abilitata, gli utenti nei gruppi consentiti possono menzionare questo utente nei post, l'IA risponderà come questa persona.
- user: Utente
- create_user: Crea utente
- create_user_help: Facoltativamente, è possibile associare un utente a questa persona. In tal caso, l'IA utilizzerà questo utente per rispondere alle richieste.
- default_llm: Modello linguistico predefinito
- default_llm_help: Il modello linguistico predefinito da utilizzare per questa persona. Obbligatorio se desideri menzionare la persona nei post pubblici.
- question_consolidator_llm: Modello linguistico per il consolidatore di domande
- question_consolidator_llm_help: Il modello linguistico da utilizzare per il consolidatore di domande. È possibile scegliere un modello meno potente per risparmiare sui costi.
- system_prompt: Comando di sistema
- forced_tool_strategy: Strategia degli strumenti forzati
- allow_chat_direct_messages: "Consenti messaggi diretti in chat"
- allow_chat_direct_messages_help: "Se l'opzione è abilitata, gli utenti nei gruppi consentiti possono inviare messaggi diretti a questa persona."
- allow_chat_channel_mentions: "Consenti menzioni nei canali di chat"
- allow_chat_channel_mentions_help: "Se abilitato, gli utenti nei gruppi consentiti possono menzionare questo personaggio nei canali di chat."
- allow_personal_messages: "Consenti messaggi personali"
- allow_personal_messages_help: "Se l'opzione è abilitata, gli utenti nei gruppi consentiti possono inviare messaggi personali a questo personaggio."
- allow_topic_mentions: "Consenti menzioni nell'argomento"
- allow_topic_mentions_help: "Se abilitato, gli utenti nei gruppi consentiti possono menzionare questa persona negli argomenti."
- force_default_llm: "Usa sempre il modello linguistico predefinito"
- save: "Salva"
- saved: "Persona salvata"
- enabled: "Abilitato?"
- tools: "Strumenti abilitati"
- forced_tools: "Strumenti forzati"
- allowed_groups: "Gruppi ammessi"
- confirm_delete: "Vuoi davvero eliminare questo personaggio?"
- new: "Nuovo personaggio"
- no_personas: "Non hai ancora creato nessun personaggio"
- title: "Personaggi"
- short_title: "Personaggi"
- delete: "Elimina"
- temperature: "Temperatura"
- temperature_help: "Temperatura da utilizzare per LLM. Aumenta per aumentare la creatività (lascia vuoto per utilizzare il modello predefinito, generalmente un valore compreso tra 0,0 e 2,0)"
- top_p: "P superiore"
- top_p_help: "P superiore da utilizzare per LLM, aumenta per aumentare la casualità (lascia vuoto per utilizzare l'impostazione predefinita del modello, generalmente un valore compreso tra 0,0 e 1,0)"
- priority: "Priorità"
- priority_help: "I personaggi prioritari vengono visualizzati agli utenti nella parte superiore dell'elenco dei personaggi. Se più personaggi hanno la priorità, verranno ordinati in ordine alfabetico."
- tool_options: "Opzioni dello strumento"
- rag_conversation_chunks: "Cerca blocchi di conversazione"
- rag_conversation_chunks_help: "Il numero di blocchi da utilizzare per le ricerche del modello RAG. Aumenta per aumentare la quantità di contesto che l'IA può utilizzare."
- persona_description: "I personaggi sono una potente funzionalità che ti consente di personalizzare il comportamento del motore IA nel tuo forum Discourse. Agiscono come un \"messaggio di sistema\" che guida le risposte e le interazioni dell'IA, aiutando a creare un'esperienza utente più personalizzata e coinvolgente."
- response_format:
- open_modal: "Modifica"
- modal:
- key_title: "Chiave"
- filters:
- reset: "Annulla"
- rag:
- options:
- rag_chunk_tokens: "Carica token di blocco"
- rag_chunk_tokens_help: "Il numero di token da utilizzare per ogni blocco nel modello RAG. Aumenta per aumentare la quantità di contesto che l'IA può utilizzare. (La modifica reindicizzerà tutti i caricamenti)"
- rag_chunk_overlap_tokens: "Carica token di sovrapposizione di blocco"
- rag_chunk_overlap_tokens_help: "Il numero di token da sovrapporre tra i blocchi nel modello RAG. (La modifica reindicizzerà tutti i caricamenti)"
- show_indexing_options: "Mostra opzioni di caricamento"
- hide_indexing_options: "Nascondi opzioni di caricamento"
- uploads:
- title: "Caricamenti"
- button: "Aggiungi file"
- filter: "Filtro caricamenti"
- indexed: "Indicizzato"
- indexing: "Indicizzazione"
- uploaded: "Pronto per essere indicizzato"
- uploading: "Caricamento..."
- remove: "Rimuovi caricamento"
- tools:
- back: "Indietro"
- short_title: "Strumenti"
- export: "Esporta"
- no_tools: "Non hai ancora creato nessuno strumento"
- name: "Nome"
- new: "Nuovo strumento"
- description: "Descrizione"
- description_help: "Una descrizione chiara dello scopo dello strumento per il modello linguistico"
- subheader_description: "Gli strumenti estendono le capacità dei bot di intelligenza artificiale con funzioni JavaScript definite dall'utente."
- summary: "Riepilogo"
- summary_help: "Il riepilogo degli strumenti ha lo scopo di essere visualizzato agli utenti finali"
- script: "Script"
- parameters: "Parametri"
- save: "Salva"
- remove_parameter: "Rimuovi"
- parameter_required: "Obbligatorie"
- parameter_enum: "Enumerazione"
- parameter_name: "Nome del parametro"
- parameter_description: "Descrizione del parametro"
- enum_value: "Valore enumerativo"
- add_enum_value: "Aggiungi valore enumerativo"
- edit: "Modifica"
- test: "Esegui test"
- delete: "Elimina"
- saved: "Strumento salvato"
- confirm_delete: "Vuoi davvero eliminare questo strumento?"
- test_modal:
- title: "Prova lo strumento IA"
- run: "Esegui test"
- result: "Risultato del test"
- llms:
- short_title: "LLM"
- no_llms: "Ancora nessun LLM"
- new: "Nuovo modello"
- display_name: "Nome"
- name: "ID modello"
- provider: "Fornitore"
- tokenizer: "Tokenizzatore"
- url: "URL del servizio che ospita il modello"
- api_key: "Chiave API del servizio che ospita il modello"
- enabled_chat_bot: "Consenti selettore bot IA"
- vision_enabled: "Visione abilitata"
- ai_bot_user: "Utente bot IA"
- save: "Salva"
- edit: "Modifica"
- saved: "Modello LLM salvato"
- back: "Indietro"
- confirm_delete: Vuoi davvero eliminare questo modello?
- delete: Elimina
- seeded_warning: "Questo modello è preconfigurato sul tuo sito e non può essere modificato."
- quotas:
- title: "Quote di utilizzo"
- add_title: "Crea nuova quota"
- group: "Gruppo"
- max_tokens: "Numero massimo di token"
- max_usages: "Limite max di utilizzi"
- duration: "Durata"
- confirm_delete: "Vuoi davvero eliminare questa quota?"
- add: "Aggiungi quota"
- durations:
- hour: "1 ora"
- six_hours: "6 ore"
- day: "24 ore"
- week: "7 giorni"
- custom: "Personalizza..."
- hours: "ore"
- max_tokens_help: "Numero massimo di token (parole e caratteri) che ogni utente di questo gruppo può utilizzare entro la durata specificata. I token sono le unità utilizzate dai modelli IA per elaborare il testo: circa 1 token = 4 caratteri o 3/4 di parola."
- max_usages_help: "Numero massimo di volte in cui ogni utente in questo gruppo può usare il modello IA entro la durata specificata. Questa quota viene tracciata per singolo utente, non condivisa tra il gruppo."
- usage:
- ai_bot: "Bot IA"
- ai_helper: "Assistente"
- ai_persona: "Personaggio (%{persona})"
- ai_summarization: "Riassumi"
- ai_embeddings_semantic_search: "Ricerca IA"
- ai_spam: "Spam"
- in_use_warning:
- one: "Questo modello è attualmente utilizzato da %{settings}. Se configurato in modo errato, la funzionalità non funzionerà come previsto."
- other: "Questo modello è attualmente utilizzato da quanto segue: %{settings}. Se configurato in modo errato, le funzionalità non funzioneranno come previsto. "
- model_description:
- none: "Impostazioni generali che vanno bene per la maggior parte dei modelli linguistici"
- anthropic-claude-opus-4-0: "Il modello più intelligente di Anthropic"
- anthropic-claude-3-5-haiku-latest: "Veloce e conveniente"
- google-gemini-2-5-flash: "Leggero, veloce ed economico con ragionamento multimodale"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Modello multilingue leggero ed efficiente"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Potente modello multifunzionale"
- mistral-mistral-large-latest: "Il modello più potente di Mistral"
- mistral-pixtral-large-latest: "Il modello con capacità di visione più potente di Mistral"
- preseeded_model_description: "Modello open source preconfigurato che utilizza %{model}"
- configured:
- title: "LLM configurati"
- preconfigured_llms: "Seleziona il tuo LLM"
- preconfigured:
- title_no_llms: "Seleziona un modello per iniziare"
- title: "Modelli LLM non configurati"
- description: "Gli LLM (Large Language Models) sono strumenti di intelligenza artificiale ottimizzati per attività quali la sintesi dei contenuti, la generazione di report, l'automazione delle interazioni con i clienti e la facilitazione della moderazione e degli approfondimenti dei forum"
- fake: "Configurazione manuale"
- button: "Configura"
- next:
- title: "Avanti"
- tests:
- title: "Esegui test"
- running: "Esecuzione del test..."
- success: "Riuscito!"
- failure: "Il tentativo di contattare il modello ha restituito questo errore: %{error}"
- hints:
- name: "Lo includiamo nella chiamata API per specificare quale modello utilizzeremo"
- vision_enabled: "Se l'opzione è abilitata, l'intelligenza artificiale tenterà di comprendere le immagini. Dipende dal modello utilizzato per supportare la visione. Supportato dagli ultimi modelli di Anthropic, Google e OpenAI."
- enabled_chat_bot: "Se abilitato, gli utenti possono selezionare questo modello quando creano MP con il bot IA"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Personalizzato"
- provider_fields:
- access_key_id: "ID chiave di accesso AWS Bedrock"
- region: "Regione AWS Bedrock"
- organization: "ID organizzazione OpenAI facoltativo"
- disable_system_prompt: "Disabilita il messaggio di sistema nei comandi"
- enable_native_tool: "Abilita il supporto degli strumenti nativi"
- disable_native_tools: "Disabilita il supporto degli strumenti nativi (utilizza strumenti basati su XML)"
- provider_order: "Ordine fornitori (elenco delimitato da virgole)"
- provider_quantizations: "Ordine delle quantizzazioni dei fornitori (elenco delimitato da virgole, ad esempio: fp16, fp8)"
- disable_streaming: "Disabilita i completamenti dello streaming (converti le richieste di streaming in richieste non di streaming)"
- related_topics:
- title: "Argomenti correlati"
- pill: "Correlato"
- ai_helper:
- title: "Suggerisci modifiche utilizzando l'IA"
- description: "Scegli una delle opzioni seguenti e l'IA ti suggerirà una nuova versione del testo."
- selection_hint: "Suggerimento: puoi anche selezionare una parte del testo prima di aprire l'assistente per riscrivere solo quel pezzo."
- suggest: "Suggerisci con l'IA"
- suggest_errors:
- too_many_tags:
- one: "Puoi avere solo fino a %{count} etichetta"
- other: "Puoi avere solo fino a %{count} etichette"
- no_suggestions: "Nessun suggerimento disponibile"
- missing_content: "Inserisci alcuni contenuti per generare suggerimenti."
- context_menu:
- trigger: "Chiedi all'IA"
- loading: "L'IA sta generando"
- cancel: "Annulla"
- confirm: "Conferma"
- discard: "Elimina"
- changes: "Modifiche suggerite"
- custom_prompt:
- title: "Comando personalizzato"
- placeholder: "Inserisci un comando personalizzato..."
- submit: "Invia comando"
- translate_prompt: "Traduci in %{language}"
- post_options_menu:
- trigger: "Chiedi all'IA"
- title: "Chiedi all'IA"
- loading: "L'IA sta generando"
- close: "Chiudi"
- copy: "Copia"
- copied: "Copiato!"
- cancel: "Annulla"
- insert_footnote: "Aggiungi nota a piè di pagina"
- footnote_disabled: "Inserimento automatico disabilitato, clicca sul pulsante Copia e modificalo manualmente"
- footnote_credits: "Spiegazione dell'IA"
- fast_edit:
- suggest_button: "Suggerisci modifica"
- thumbnail_suggestions:
- title: "Miniature suggerite"
- select: "Seleziona"
- selected: "Selezionato"
- image_caption:
- button_label: "Didascalia con IA"
- generating: "Generazione didascalia in corso..."
- credits: "Didascalia da IA"
- save_caption: "Salva"
- automatic_caption_setting: "Abilita sottotitoli automatici"
- automatic_caption_loading: "Generazione delle didascalie delle immagini..."
- automatic_caption_dialog:
- prompt: "Questo post contiene immagini senza didascalie. Desideri abilitare le didascalie automatiche sui caricamenti di immagini? (Questa opzione può essere modificata nelle tue preferenze in seguito)"
- confirm: "Abilita"
- cancel: "Non chiedermelo più"
- no_content_error: "Aggiungi prima il contenuto per eseguire azioni IA su di esso"
- reviewables:
- model_used: "Modello utilizzato:"
- accuracy: "Precisione:"
- embeddings:
- short_title: "Integrazioni"
- new: "Nuova integrazione"
- back: "Indietro"
- save: "Salva"
- saved: "Configurazione di integrazione salvata"
- delete: "Elimina"
- confirm_delete: Vuoi davvero rimuovere questa configurazione di integrazione?
- empty: "Non hai ancora impostato le integrazioni"
- presets: "Seleziona una preimpostazione..."
- configure_manually: "Configura manualmente"
- edit: "Modifica"
- seeded_warning: "Questo elemento è preconfigurato sul tuo sito e non può essere modificato."
- tests:
- title: "Esegui test"
- running: "Esecuzione del test..."
- success: "Operazione riuscita!"
- failure: "Il tentativo di generare un'integrazione ha prodotto: %{error}"
- hints:
- dimensions_warning: "Una volta salvato, questo valore non può essere modificato."
- matryoshka_dimensions: "Definisce la dimensione delle integrazioni nidificate utilizzate per la rappresentazione gerarchica o multistrato dei dati, in modo simile a come le matrioske si inseriscono l'una nell'altra."
- sequence_length: "Numero massimo di token che possono essere elaborati contemporaneamente durante la creazione di integrazioni o la gestione di una query."
- distance_function: "Determina come viene calcolata la similarità tra integrazioni, utilizzando la distanza del coseno (misurando l'angolo tra i vettori) o il prodotto interno negativo (misurando la sovrapposizione dei valori dei vettori)."
- display_name: "Nome"
- provider: "Fornitore"
- url: "URL del servizio di integrazione"
- api_key: "Chiave API del servizio di integrazione"
- tokenizer: "Tokenizzatore"
- dimensions: "Dimensioni dell'integrazione"
- max_sequence_length: "Lunghezza della sequenza"
- embed_prompt: "Comando di integrazione"
- search_prompt: "Comando di ricerca"
- matryoshka_dimensions: "Dimensioni della matrioska"
- distance_function: "Funzione di distanza"
- distance_functions:
- "<#>": "Prodotto interno negativo"
- <=>: "Distanza del coseno"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Personalizzato"
- provider_fields:
- model_name: "Nome del modello"
- semantic_search: "Argomenti (semantici)"
- semantic_search_loading: "Ricerca di altri risultati tramite intelligenza artificiale"
- semantic_search_results:
- toggle: "Stai vedendo %{count} risultati trovati utilizzando l'IA"
- toggle_hidden: "%{count} risultati trovati utilizzando l'IA sono stati nascosti"
- none: "Spiacenti, la nostra ricerca IA non ha trovato argomenti corrispondenti"
- new: "Premi \"cerca\" per iniziare a cercare nuovi risultati con l'intelligenza artificiale"
- unavailable: "Risultati IA non disponibili"
- semantic_search_tooltips:
- results_explanation: "Se abilitata, verranno aggiunti ulteriori risultati di ricerca IA di seguito."
- invalid_sort: "I risultati della ricerca devono essere ordinati in base alla pertinenza per visualizzare i risultati IA"
- semantic_search_unavailable_tooltip: "I risultati della ricerca devono essere ordinati in base alla pertinenza per visualizzare i risultati IA"
- ai_generated_result: "Risultato della ricerca trovato utilizzando l'intelligenza artificiale"
- quick_search:
- suffix: "in tutti gli argomenti e i post con IA"
- ai_artifact:
- expand_view_label: "Espandi vista"
- collapse_view_label: "Esci dalla modalità a schermo intero (tasto ESC o Indietro)"
- click_to_run_label: "Esegui artefatto"
- ai_bot:
- llm: "Modello"
- pm_warning: "I messaggi del chatbot IA vengono controllati regolarmente dai moderatori."
- cancel_streaming: "Interrompi risposta"
- default_pm_prefix: "[Bot IA senza titolo MP]"
- shortcut_title: "Avvia un MP con un bot IA"
- share: "Copia la conversazione IA"
- conversation_shared: "Conversazione copiata"
- debug_ai: "Visualizza la richiesta e la risposta IA non elaborate"
- debug_ai_modal:
- title: "Visualizza l'interazione dell'IA"
- copy_request: "Copia richiesta"
- copy_response: "Copia risposta"
- request_tokens: "Token richieste:"
- response_tokens: "Token risposte:"
- request: "Richiesta"
- response: "Risposta"
- next_log: "Avanti"
- previous_log: "Precedente"
- share_full_topic_modal:
- title: "Condividi la conversazione pubblicamente"
- share: "Condividi e copia il link"
- update: "Aggiorna e copia il link"
- delete: "Elimina condivisione"
- share_ai_conversation:
- name: "Condividi la conversazione con IA"
- title: "Condividi pubblicamente questa conversazione IA"
- invite_ai_conversation:
- button: "Invita"
- ai_label: "IA"
- ai_title: "Conversazione con IA"
- share_modal:
- title: "Copia la conversazione IA"
- copy: "Copia"
- context: "Interazioni da condividere:"
- share_tip: "In alternativa, puoi condividere l'intera conversazione"
- bot_names:
- fake: "Bot di prova finto"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "Oggi"
- last_7_days: "Ultimi 7 giorni"
- last_30_days: "Ultimi 30 giorni"
- sentiments:
- dashboard:
- title: "Sentimento"
- sentiment_analysis:
- filter_types:
- all: "Tutte"
- positive: "Positiva"
- neutral: "Neutro"
- negative: "Negativa"
- group_types:
- category: "Categoria"
- tag: "Etichetta"
- table:
- sentiment: "Sentimento"
- total_count: "Totali"
- summarization:
- chat:
- title: "Riassumi i messaggi"
- description: "Seleziona un'opzione qui sotto per riepilogare la conversazione inviata nel periodo di tempo desiderato."
- summarize: "Riassumi"
- since:
- one: "Ultima ora"
- other: "Ultime %{count} ore"
- topic:
- title: "Riepilogo dell'argomento"
- close: "Chiudi il pannello riassuntivo"
- topic_list_layout:
- button:
- compact: "Compatto"
- expanded: "Espanso"
- expanded_description: "con riepiloghi IA"
- discobot_discoveries:
- regular_results: "Argomenti"
- collapse: "Comprimi"
- tooltip:
- actions:
- disable: "Disattiva"
- review:
- types:
- reviewable_ai_post:
- title: "Messaggio contrassegnato da IA"
- reviewable_ai_chat_message:
- title: "Messaggio di chat contrassegnato da IA"
diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml
deleted file mode 100644
index 1e795a3b..00000000
--- a/config/locales/client.ja.yml
+++ /dev/null
@@ -1,683 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ja:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "AI 検索を許可します"
- stream_completion: "ストリーミング AI ペルソナの補完を許可します"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "感情"
- description: "表には、特定の感情で分類された投稿のかっずがリストされます。'SamLowe/roberta-base-go_emotions' モデルで分類されます。"
- reports:
- filters:
- sort_by:
- label: "並べ替え"
- tag:
- label: "タグ"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "送信者"
- description: "レポートを送信するユーザー"
- receivers:
- label: "受信者"
- description: "レポートを受け取るユーザー (メールはダイレクトメールで送信され、ユーザー名に PM が送信されます)"
- topic_id:
- label: "トピック ID"
- description: "レポートを投稿するトピック ID"
- title:
- label: "タイトル"
- description: "レポートのタイトル"
- days:
- label: "日数"
- description: "レポートの期間"
- offset:
- label: "オフセット"
- description: "テスト時にレポートを過去にさかのぼって実行する必要がある場合は、オフセットを使用してレポートをより前の日付から開始します"
- instructions:
- label: "指示"
- description: "大規模言語モデルに提供する指示"
- sample_size:
- label: "サンプルサイズ"
- description: "レポート用にサンプリングする投稿の数"
- tokens_per_post:
- label: "投稿あたりのトークン"
- description: "投稿あたりに使用する LLM トークンの数"
- model:
- label: "モデル"
- description: "レポートの生成に使用する LLM"
- categories:
- label: "カテゴリ"
- description: "トピックをこれらのカテゴリのみにフィルタします"
- tags:
- label: "タグ"
- description: "トピックをこれらのタグのみにフィルタします"
- exclude_tags:
- label: "タグを除外"
- description: "これらのタグが付いたトピックを除外します"
- exclude_categories:
- label: "カテゴリを除外"
- description: "これらのカテゴリのトピックを除外します"
- allow_secure_categories:
- label: "安全なカテゴリを許可する"
- description: "安全なカテゴリに含まれるトピックに対しレポートの生成を許可します"
- suppress_notifications:
- label: "通知を非表示"
- description: "コンテンツに変換することでレポートが生成する可能性のある通知を非表示にします。これにより、メンションと内部リンクが再マッピングされます。"
- debug_mode:
- label: "デバッグモード"
- description: "デバッグモードを有効にして、LLM の生の入力と出力を確認します"
- priority_group:
- label: "優先グループ"
- description: "レポートでこのグループのコンテンツを優先します"
- temperature:
- label: "Temperature"
- top_p:
- label: "Top P"
- llm_tool_triage:
- fields:
- model:
- label: "モデル"
- llm_triage:
- fields:
- system_prompt:
- label: "システムプロンプト"
- description: "トリアージに使用されるプロンプト。ユーザーがアクションのトリガーに使用できる 1 つの単語でプロンプトが応答できるようにしてください"
- max_post_tokens:
- label: "最大投稿トークン"
- description: "LLM トリアージを使用してスキャンするトークンの最大数"
- stop_sequences:
- label: "シーケンスを停止"
- description: "これらの値のいずれかに到達したらトークンの生成を停止するようにモデルに指示します"
- search_for_text:
- label: "テキストを検索する"
- description: "LLM の返信に後続のテキストが表示される場合は、これらのアクションを適用します"
- category:
- label: "カテゴリ"
- description: "トピックに適用するカテゴリ"
- tags:
- label: "タグ"
- description: "トピックに適用するタグ"
- canned_reply:
- label: "返信"
- description: "トピックに投稿する定型返信の生のテキスト"
- canned_reply_user:
- label: "ユーザーに返信"
- description: "定型返信を投稿するユーザーのユーザー名"
- hide_topic:
- label: "トピックを非表示"
- description: "トリガーされると、トピックを一般公開しないようにします"
- flag_type:
- label: "通報タイプ"
- description: "投稿に適用する通報のタイプ (迷惑または単なるレビュー対象)"
- flag_post:
- label: "投稿を通報"
- description: "投稿を通報 (迷惑またはレビュー対象)"
- include_personal_messages:
- label: "個人メッセージを含める"
- description: "個人メッセージのスキャンとトリアージも行う"
- model:
- label: "モデル"
- description: "トリアージに使用される言語モデル"
- temperature:
- label: "Temperature"
- discourse_ai:
- title: "AI"
- features:
- back: "戻る"
- disabled: "(無効)"
- persona:
- other: "ペルソナ:"
- groups: "グループ:"
- llm:
- other: "LLM:"
- no_persona: "未設定"
- no_groups: "なし"
- edit: "編集"
- expand_list:
- other: "(他 %{count})"
- collapse_list: "(表示を減らす)"
- filters:
- all: "すべて"
- reset: "リセット"
- search:
- name: "検索"
- embeddings:
- name: "埋め込み"
- ai_helper:
- name: "ヘルパー"
- proofread: テキストを構成
- explain: "説明"
- smart_dates: "スマート日付"
- markdown_tables: "Markdown テーブルを生成"
- custom_prompt: "カスタムプロンプト"
- spam:
- name: "迷惑"
- description: "選択された LLM を使って潜在的な迷惑行為を識別し、サイトモデレーターがレビューキューで検査できるように通報します"
- modals:
- select_option: "オプションを選択..."
- spam:
- short_title: "迷惑"
- title: "迷惑処理の構成"
- select_llm: "LLM を選択してください"
- custom_instructions: "カスタム命令"
- custom_instructions_help: "AI による迷惑行為の識別を誘導しやすくするサイトに固有のカスタム命令。例: 「英語以外の投稿をより積極的にスキャンする」。"
- last_seven_days: "過去 7 日間"
- scanned_count: "スキャンされた投稿数"
- false_positives: "誤った通報"
- false_negatives: "取り逃した迷惑行為"
- spam_detected: "迷惑行為が検出されました"
- custom_instructions_placeholder: "AI が迷惑行為をより正確に識別できるようにするためのサイト固有の命令"
- enable: "有効化"
- spam_tip: "AI 迷惑検出機能は、公開トピックのすべての新規ユーザーによる最初の 3 件の投稿をスキャンします。レビューを行うために通報し、迷惑の可能性がある場合はユーザーをブロックします。"
- settings_saved: "設定が保存されました"
- spam_description: "選択された LLM を使って潜在的な迷惑行為を識別し、サイトモデレーターがレビューキューで検査できるように通報します"
- no_llms: "使用できる LLM はありません"
- test_button: "テスト..."
- save_button: "変更を保存"
- test_modal:
- title: "迷惑検出のテスト"
- post_url_label: "投稿の URL または ID"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 または投稿 ID"
- result: "結果"
- scan_log: "スキャンログ"
- run: "テストの実行"
- spam: "迷惑"
- not_spam: "迷惑ではない"
- stat_tooltips:
- incorrectly_flagged: "AI ボットが迷惑として通報し、モデレーターがそれに同意しなかった項目"
- missed_spam: "AI ボットによって迷惑として検出されなかったが、コミュニティーは迷惑として通報し、モデレーターもそれに同意した項目"
- errors:
- scan_not_admin:
- message: "警告: 迷惑スキャンアカウントが管理者ではないため、迷惑スキャンは正しく機能しません"
- action: "修正"
- resolved: "エラーは解決されました!"
- usage:
- short_title: "使用状況"
- summary: "要約"
- total_tokens: "合計トークン"
- tokens_over_time: "経時的なトークン使用状況"
- features_breakdown: "機能ごとの使用状況"
- feature: "機能"
- usage_count: "使用回数"
- model: "モデル"
- models_breakdown: "モデルごとの使用状況"
- users_breakdown: "ユーザーごとの使用状況"
- all_features: "すべての機能"
- all_models: "すべてのモデル"
- username: "ユーザー名"
- total_requests: "合計リクエスト"
- request_tokens: "リクエストトークン"
- response_tokens: "レスポンストークン"
- net_request_tokens: "ネットリクエストトークン"
- cached_tokens: "キャッシュされたトークン"
- cached_request_tokens: "キャッシュされたリクエストトークン"
- no_users: "ユーザー使用状況データが見つかりません"
- no_models: "モデル使用状況データが見つかりません"
- no_features: "機能使用状況データが見つかりません"
- stat_tooltips:
- total_requests: "Discourse を通じて LLM に行われたすべてのリクエスト"
- total_tokens: "LLM のプロンプトに使用されるすべてのトークン"
- request_tokens: "ユーザーが述べることを LLM が理解しようとする際に使用されるトークン"
- response_tokens: "LLM がプロンプトに応答する際に使用されるトークン"
- cached_tokens: "LLM がパフォーマンスとコストを最適化するために再利用する、過去に処理されたリクエストトークン"
- periods:
- last_day: "過去 24 時間"
- last_week: "先週"
- last_month: "先月"
- custom: "カスタム..."
- ai_persona:
- ai_tools: "ツール"
- tool_strategies:
- all: "すべての返信に適用"
- replies:
- other: "最初の %{count} 件の返信に適用"
- back: "戻る"
- name: "名前"
- edit: "編集"
- export: "エクスポート"
- description: "説明"
- no_llm_selected: "言語モデルは選択されていません"
- max_context_posts: "最大コンテキスト投稿数"
- max_context_posts_help: "AI がユーザーに返答するときにコンテキストとして使用する投稿の最大数。(デフォルトの場合は空白)"
- vision_enabled: ビジョン対応
- vision_enabled_help: 有効にすると、AI は、ビジョンのサポートに使用されているモデルに応じてユーザーがトピックに投稿する画像を理解しようとします。Anthropic、Google、および OpenAI の最新モデルでサポートされています。
- vision_max_pixels: サポートされている画像サイズ
- vision_max_pixel_sizes:
- low: 低品質 - 最割安 (256x256)
- medium: 中品質 (512x512)
- high: 高品質 - 最も遅い (1024x1024)
- tool_details: ツールの詳細を表示
- tool_details_help: 言語モデルがトリガーしたツールの詳細をエンドユーザーに表示します。
- mentionable: メンションを許可
- mentionable_help: 有効にすると、許可されているグループのユーザーは投稿内でこのユーザーをメンションでき、AI はこのペルソナとして返答します。
- user: ユーザー
- create_user: ユーザーを作成
- create_user_help: オプションで、このペルソナにユーザーを関連付けられます。その場合、AI はこのユーザーを使用してリクエストに応答します。
- default_llm: デフォルトの言語モデル
- default_llm_help: このペルソナに使用するデフォルトの言語モデル。公開投稿でペルソナをメンションする場合には必須です。
- question_consolidator_llm: 質問統合用の言語モデル
- question_consolidator_llm_help: 質問統合に使用する言語モデル。それほど強力でないモデルを選択してコストを節約することも可能です。
- system_prompt: システムプロンプト
- forced_tool_strategy: 強制されるツール戦略
- allow_chat_direct_messages: "チャットダイレクトメッセージを許可"
- allow_chat_direct_messages_help: "有効にすると、許可されているグループのユーザーはこのペルソナにダイレクトメッセージを送信できます。"
- allow_chat_channel_mentions: "チャットチャンネルのメンションを許可"
- allow_chat_channel_mentions_help: "有効にすると、許可されたグループ内のユーザーはチャットチャンネルでこのペルソナをメンションできます。"
- allow_personal_messages: "個人メッセージを許可"
- allow_personal_messages_help: "有効にすると、許可されているグループのユーザーはこのペルソナに個人メッセージを送信できます。"
- allow_topic_mentions: "トピックのメンションを許可"
- allow_topic_mentions_help: "有効にすると、許可されたグループ内のユーザーはトピックでこのペルソナをメンションできます。"
- force_default_llm: "常にデフォルトの言語モデルを使用する"
- save: "保存"
- saved: "ペルソナが保存されました"
- enabled: "有効化?"
- tools: "有効なツール"
- forced_tools: "強制されたツール"
- allowed_groups: "許可されたグループ"
- confirm_delete: "このペルソナを削除してもよろしいですか?"
- new: "新しいペルソナ"
- no_personas: "ペルソナをまだ作成していません"
- title: "ペルソナ"
- short_title: "ペルソナ"
- delete: "削除"
- temperature: "Temperature"
- temperature_help: "LLM に使用する Temperature。値を増やすと創造性が増加します (モデルのデフォルトを使用する場合は空白にします。一般に 0.0~2.0 の値です)"
- top_p: "Top P"
- top_p_help: "LLM に使用する Top P。値を増やすとランダム性が増加します (モデルのデフォルトを使用する場合は空白にします。一般に 0.0~1.0 の値です)"
- priority: "優先度"
- priority_help: "優先ペルソナはペルソナリストの先頭に表示されます。複数のペルソナが優先されている場合は、アルファベット順に並べ替えられます。"
- tool_options: "ツールのオプション"
- rag_conversation_chunks: "会話チャンクを検索"
- rag_conversation_chunks_help: "RAG モデル検索に使用するチャンクの数。値を増やすと、AI が使用できるコンテキストの量が増えます。"
- persona_description: "ペルソナは、Discourse フォーラムの AI エンジンの動作をカスタマイズできる強力な機能です。AI の応答と対話を誘導する「システムメッセージ」として機能し、よりパーソナライズされた魅力的なユーザーエクスペリエンスの作成に役立ちます。"
- response_format:
- open_modal: "編集"
- modal:
- key_title: "キー"
- filters:
- reset: "リセット"
- rag:
- options:
- rag_chunk_tokens: "チャンクトークンをアップロード"
- rag_chunk_tokens_help: "RAG モデルの各チャンクに使用するトークン数。値を増やすと、AI が使用できるコンテキストの数が増加します (変更するとすべてのアップロードのインデックスが再作成されます)"
- rag_chunk_overlap_tokens: "チャンクオーバーラップトークンをアップロード"
- rag_chunk_overlap_tokens_help: "RAG モデル内のチャンク間で重複するトークンの数。(変更するとすべてのアップロードのインデックスが再作成されます)"
- show_indexing_options: "アップロードオプションを表示"
- hide_indexing_options: "アップロードオプションを非表示"
- uploads:
- title: "アップロード"
- button: "ファイルを追加"
- filter: "アップロードをフィルタ"
- indexed: "インデックスを作成しました"
- indexing: "インデックス作成中"
- uploaded: "インデックス作成の準備ができました"
- uploading: "アップロード中..."
- remove: "アップロードを削除"
- tools:
- back: "戻る"
- short_title: "ツール"
- export: "エクスポート"
- no_tools: "ツールをまだ作成していません"
- name: "名前"
- new: "新しいツール"
- description: "説明"
- description_help: "言語モデルに対するツールの目的の明確な説明"
- subheader_description: "ツールは、ユーザー定義の JavaScript 関数を使用して、AI ボットの機能を拡張します。"
- summary: "要約"
- summary_help: "エンドユーザーに表示されるツールの目的の要約"
- script: "スクリプト"
- parameters: "パラメーター"
- save: "保存"
- remove_parameter: "削除"
- parameter_required: "必須"
- parameter_enum: "列挙型"
- parameter_name: "パラメーター名"
- parameter_description: "パラメーターの説明"
- enum_value: "列挙型値"
- add_enum_value: "列挙型値を追加"
- edit: "編集"
- test: "テストを実行"
- delete: "削除"
- saved: "ツールが保存されました"
- confirm_delete: "このツールを削除してもよろしいですか?"
- test_modal:
- title: "AI ツールのテスト"
- run: "テストを実行"
- result: "テストの結果"
- llms:
- short_title: "LLM"
- no_llms: "まだ LLM がありません"
- new: "新しいモデル"
- display_name: "名前"
- name: "モデル ID"
- provider: "プロバイダー"
- tokenizer: "トークナイザ―"
- url: "モデルをホストするサービスの URL"
- api_key: "モデルをホストするサービスの API キー"
- enabled_chat_bot: "AI ボットのセレクターを許可"
- vision_enabled: "ビジョン対応"
- ai_bot_user: "AI ボットユーザー"
- save: "保存"
- edit: "編集"
- saved: "LLM モデルが保存されました"
- back: "戻る"
- confirm_delete: このモデルを削除してもよろしいですか?
- delete: 削除
- seeded_warning: "このモデルはサイト上で事前設定されているため、編集できません。"
- quotas:
- title: "使用量制限"
- add_title: "新しい制限の作成"
- group: "グループ"
- max_tokens: "トークン上限"
- max_usages: "最大使用回数"
- duration: "期間"
- confirm_delete: "この制限を削除してもよろしいですか?"
- add: "制限を追加"
- durations:
- hour: "1時間"
- six_hours: "6 時間"
- day: "24 時間"
- week: "7 日間"
- custom: "カスタム..."
- hours: "時間"
- max_tokens_help: "このグループの各ユーザーが指定された期間内に使用できるトークン(単語と文字)の最大数。トークンは、AI モデルがテキストを処理するときに使用する単位です。およそ 1 トークン = 4 文字または 1 単語の 3/4 です。"
- max_usages_help: "このグループの各ユーザーが指定された期間内に AI モデルを使用できる最大回数。この使用量制限はグループ全体で共有されるのではなく、ユーザーごとに追跡されます。"
- usage:
- ai_bot: "AI ボット"
- ai_helper: "ヘルパー"
- ai_persona: "ペルソナ (%{persona})"
- ai_summarization: "要約"
- ai_embeddings_semantic_search: "AI 検索"
- ai_spam: "迷惑"
- in_use_warning:
- other: "このモデルは現在次によって使用されています: %{settings}。誤って構成されると、機能は期待どおりに動作しなくなります。"
- model_description:
- none: "ほとんどの言語モデルで機能する一般的な設定"
- anthropic-claude-opus-4-0: "Anthropic の最もインテリジェントなモデル"
- anthropic-claude-3-5-haiku-latest: "高速でコスト効率に優れています"
- google-gemini-2-5-flash: "マルチモーダル推論による軽量・高速で、コスト効率に優れています"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "効率的な軽量多言語モデル"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "強力な多目的モデル"
- mistral-mistral-large-latest: "Mistral の最も強力なモデル"
- mistral-pixtral-large-latest: "Mistral の最も強力なビジョン対応モデル"
- preseeded_model_description: "%{model} を利用する事前構成済みのオープンソースモデル"
- configured:
- title: "構成済みの LLM"
- preconfigured_llms: "LLM を選択してください"
- preconfigured:
- title_no_llms: "開始するにはテンプレートを選択してください"
- title: "未構成の LLM テンプレート"
- fake: "手動構成"
- button: "セットアップ"
- next:
- title: "次へ"
- tests:
- title: "テストの実行"
- running: "テストを実行中…"
- success: "成功!"
- failure: "モデルに接続しようとした際に、次のエラーが返されました: %{error}"
- hints:
- name: "これを API 呼び出しに含めて、使用するモデルを指定します"
- vision_enabled: "有効にすると、AI は画像を理解しようとします。ビジョンのサポートに使用されているモデルに応じます。Anthropic、Google、および OpenAI の最新モデルでサポートされています。"
- enabled_chat_bot: "有効にすると、ユーザーは AI ボットを使って PM を作成するときに、このモデルを選択できます"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "カスタム"
- provider_fields:
- access_key_id: "AWS Bedrock アクセスキー ID"
- region: "AWS Bedrock リージョン"
- organization: "オプションの OpenAI 組織 ID"
- disable_system_prompt: "プロンプトでシステムメッセージを無効にする"
- enable_native_tool: "ネイティブツールのサポートを有効にする"
- disable_native_tools: "ネイティブツールのサポートを無効にする (XML ベースのツールを使用する)"
- provider_order: "プロバイダーの順序 (カンマ区切りリスト)"
- provider_quantizations: "プロバイダーの量子化の順序 (カンマ区切りリスト 例: fp16,fp8)"
- disable_streaming: "ストリーミングの完了を無効にする (ストリーミングを非ストリーミングリクエストに変換する)"
- related_topics:
- title: "関連トピック"
- pill: "関連"
- ai_helper:
- title: "AI を使用して変更を提案"
- description: "以下のいずれかのオプションを選択すると、AI が新しいバージョンのテキストを提案します。"
- selection_hint: "ヒント: ヘルパーを開く前にテキストの一部を選択すると、その部分のみを書き換えることもできます。"
- suggest: "AI で提案"
- suggest_errors:
- too_many_tags:
- other: "最大 %{count} 個のタグのみを使用できます"
- no_suggestions: "提案はありません"
- missing_content: "提案を生成するにはコンテンツを入力してください。"
- context_menu:
- trigger: "AI に尋ねる"
- loading: "AI が生成中です"
- cancel: "キャンセル"
- confirm: "確認"
- discard: "破棄"
- changes: "提案された編集"
- custom_prompt:
- title: "カスタムプロンプト"
- placeholder: "カスタムプロンプトを入力してください..."
- submit: "プロンプトを送信"
- translate_prompt: "%{language} に翻訳する"
- post_options_menu:
- trigger: "AI に尋ねる"
- title: "AI に尋ねる"
- loading: "AI が生成中です"
- close: "閉じる"
- copy: "コピー"
- copied: "コピーしました!"
- cancel: "キャンセル"
- insert_footnote: "脚注を追加"
- footnote_disabled: "自動挿入が無効です。コピーボタンをクリックして手動で編集してください"
- footnote_credits: "AI による説明"
- fast_edit:
- suggest_button: "編集を提案"
- thumbnail_suggestions:
- title: "提案されたサムネイル"
- select: "選択"
- selected: "選択済み"
- image_caption:
- button_label: "AI によるキャプション"
- generating: "キャプションを生成中..."
- credits: "AI によるキャプション"
- save_caption: "保存"
- automatic_caption_setting: "自動キャプションを有効にする"
- automatic_caption_loading: "画像のキャプションを作成中..."
- automatic_caption_dialog:
- prompt: "この投稿にはキャプションのない画像が含まれています。画像アップロード時に、自動キャプションを有効にしますか?(これは後で設定で変更できます)"
- confirm: "有効化"
- cancel: "今後表示しない"
- no_content_error: "先に AI アクションを実行するコンテンツを追加してください"
- reviewables:
- model_used: "使用モデル:"
- accuracy: "精度:"
- embeddings:
- short_title: "埋め込み"
- new: "新しい埋め込み"
- back: "戻る"
- save: "保存"
- saved: "埋め込み構成が保存されました"
- delete: "削除"
- confirm_delete: この埋め込み構成を削除してもよろしいですか?
- empty: "埋め込みはまだ構成されていません"
- presets: "プリセットを選択..."
- configure_manually: "手動で構成"
- edit: "編集"
- seeded_warning: "これはサイト上で事前設定されているため、編集できません。"
- tests:
- title: "テストを実行"
- running: "テストを実行中…"
- success: "成功!"
- failure: "埋め込みを生成しようとした際にエラーが発生しました: %{error}"
- hints:
- dimensions_warning: "この値は一度保存すると変更できません。"
- matryoshka_dimensions: "入れ子式の人形が相互に収まるのと同じように、データの階層または多層表示に使用されるネストされた埋め込みのサイズを定義します。"
- sequence_length: "埋め込みの作成やクエリの処理時に一度に処理できるとオークンの最大数。"
- distance_function: "コサイン距離(ベクトル間の角度を測定)または負の内積(ベクトル値の重なりを測定)のいずれかを使用して、埋め込み間の類似性をどのように計算するかを決定します。"
- display_name: "名前"
- provider: "プロバイダー"
- url: "埋め込みサービス URL"
- api_key: "埋め込みサービス API キー"
- tokenizer: "トークナイザ―"
- dimensions: "埋め込みの次元"
- max_sequence_length: "シーケンスの長さ"
- embed_prompt: "埋め込みプロンプト"
- search_prompt: "検索プロンプト"
- matryoshka_dimensions: "マトリョーシカの次元"
- distance_function: "距離関数"
- distance_functions:
- "<#>": "負の内積"
- <=>: "コサイン距離"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "カスタム"
- provider_fields:
- model_name: "モデル名"
- semantic_search: "トピック (セマンティック)"
- semantic_search_loading: "AI を使ってさらに多くの結果を検索中"
- semantic_search_results:
- toggle: "AI で見つかった %{count} 件の結果を表示中"
- toggle_hidden: "AI で見つかった %{count} 件の結果を非表示"
- none: "AI 検索ではどのトピックも一致しませんでした"
- new: "「検索」を押すと、AI が新しい結果を検索し始めます"
- unavailable: "AI の結果は利用できません"
- semantic_search_tooltips:
- results_explanation: "有効にすると、追加の AI 検索結果が下に追加されます。"
- invalid_sort: "AI 結果を表示するには、検索結果を関連性で並べ替える必要があります"
- semantic_search_unavailable_tooltip: "AI 結果を表示するには、検索結果を関連性で並べ替える必要があります"
- ai_generated_result: "AI で見つかった検索結果"
- quick_search:
- suffix: "AI によるすべてのトピックと投稿"
- ai_artifact:
- expand_view_label: "表示を拡大"
- collapse_view_label: "全画面表示を終了 (ESC または「戻る」ボタン)"
- click_to_run_label: "アーティファクトを実行"
- ai_bot:
- llm: "モデル"
- pm_warning: "AI チャットボットのメッセージは、モデレーターによって定期的に監視されます。"
- cancel_streaming: "返信を停止する"
- default_pm_prefix: "[無題の AI ボット PM]"
- shortcut_title: "AI ボットと PM を開始する"
- share: "AI の会話のコピー"
- conversation_shared: "会話をコピーしました"
- debug_ai: "生の AI リクエストと返答を表示する"
- debug_ai_modal:
- title: "AI インタラクションを表示"
- copy_request: "リクエストをコピー"
- copy_response: "返答をコピー"
- request_tokens: "リクエストトークン:"
- response_tokens: "レスポンストークン:"
- request: "リクエスト"
- response: "応答"
- next_log: "次へ"
- previous_log: "変更前"
- share_full_topic_modal:
- title: "会話の公開共有"
- share: "共有してリンクをコピー"
- update: "更新してリンクをコピー"
- delete: "共有を削除"
- share_ai_conversation:
- name: "AI の会話を共有"
- title: "この AI の会話を公開共有する"
- invite_ai_conversation:
- button: "招待"
- ai_label: "AI"
- ai_title: "AI との会話"
- share_modal:
- title: "AI の会話のコピー"
- copy: "コピー"
- context: "共有する対話:"
- share_tip: "または、会話全体を共有できます"
- bot_names:
- fake: "偽のテストボット"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "今日"
- last_7_days: "過去 7 日間"
- last_30_days: "過去 30 日間"
- sentiments:
- dashboard:
- title: "センチメント"
- sentiment_analysis:
- filter_types:
- all: "すべて"
- positive: "ポジティブ"
- neutral: "ニュートラル"
- negative: "ネガティブ"
- group_types:
- category: "カテゴリ"
- tag: "タグ"
- table:
- sentiment: "センチメント"
- total_count: "総合"
- summarization:
- chat:
- title: "メッセージを要約する"
- description: "以下から、希望する期間に送信される会話を要約するオプションを選択してください。"
- summarize: "要約"
- since:
- other: "過去 %{count} 時間"
- topic:
- title: "トピックの要約"
- close: "要約パネルを閉じる"
- topic_list_layout:
- button:
- compact: "コンパクト"
- expanded: "展開"
- expanded_description: "AI 要約を使用"
- discobot_discoveries:
- regular_results: "トピック"
- collapse: "折りたたむ"
- tooltip:
- actions:
- disable: "無効化"
- review:
- types:
- reviewable_ai_post:
- title: "AI が通報した投稿"
- reviewable_ai_chat_message:
- title: "AI が通報したチャットメッセージ"
diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml
deleted file mode 100644
index ee32f8fc..00000000
--- a/config/locales/client.ko.yml
+++ /dev/null
@@ -1,192 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ko:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "정렬 기준"
- tag:
- label: "태그"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "발신자"
- topic_id:
- label: "토픽 ID"
- title:
- label: "제목"
- categories:
- label: "카테고리"
- tags:
- label: "태그"
- llm_triage:
- fields:
- category:
- label: "카테고리"
- tags:
- label: "태그"
- canned_reply:
- label: "댓글쓰기"
- discourse_ai:
- features:
- back: "뒤로"
- disabled: "(비활성)"
- groups: "그룹:"
- no_groups: "없음"
- edit: "편집"
- expand_list:
- other: "(%{count}개 더보기)"
- filters:
- all: "전체"
- reset: "리셋"
- search:
- name: "검색"
- spam:
- name: "스팸"
- modals:
- select_option: "옵션 선택..."
- spam:
- short_title: "스팸"
- last_seven_days: "지난 7일"
- enable: "활성화"
- test_modal:
- spam: "스팸"
- usage:
- summary: "요약"
- username: "아이디"
- total_requests: "총 요청횟수"
- periods:
- last_day: "지난 24시간"
- custom: "사용자 지정..."
- ai_persona:
- back: "뒤로"
- name: "그룹명"
- edit: "편집"
- export: "내보내기"
- description: "내용"
- user: 사용자
- save: "저장"
- enabled: "활성화?"
- allowed_groups: "허용 된 그룹"
- delete: "삭제하기"
- response_format:
- open_modal: "편집"
- modal:
- key_title: "키"
- filters:
- reset: "리셋"
- rag:
- uploads:
- title: "업로드된 파일"
- uploading: "업로드 중..."
- tools:
- back: "뒤로"
- export: "내보내기"
- name: "그룹명"
- description: "내용"
- summary: "요약"
- save: "저장"
- remove_parameter: "제거"
- parameter_required: "필수"
- edit: "편집"
- delete: "삭제하기"
- llms:
- display_name: "그룹명"
- save: "저장"
- edit: "편집"
- back: "뒤로"
- delete: 삭제하기
- quotas:
- group: "그룹"
- max_usages: "최대 사용"
- duration: "기간"
- durations:
- hour: "1시간"
- six_hours: "6시간"
- day: "24시간"
- custom: "사용자 지정..."
- hours: "시간"
- usage:
- ai_summarization: "요약하기"
- ai_spam: "스팸"
- next:
- title: "다음"
- tests:
- success: "성공!"
- providers:
- google: "구글"
- fake: "사용자 정의"
- ai_helper:
- context_menu:
- cancel: "취소"
- confirm: "확인"
- discard: "포기"
- post_options_menu:
- close: "닫기"
- copy: "복사"
- copied: "복사되었습니다!"
- cancel: "취소"
- thumbnail_suggestions:
- select: "선택"
- image_caption:
- save_caption: "저장"
- automatic_caption_dialog:
- confirm: "활성화"
- embeddings:
- back: "뒤로"
- save: "저장"
- delete: "삭제하기"
- edit: "편집"
- tests:
- success: "성공!"
- display_name: "그룹명"
- providers:
- google: "구글"
- fake: "사용자 정의"
- ai_bot:
- debug_ai_modal:
- request: "요청"
- response: "응답"
- next_log: "다음"
- previous_log: "이전값"
- invite_ai_conversation:
- button: "초대"
- share_modal:
- copy: "복사"
- conversations:
- today: "오늘"
- last_7_days: "지난 7일"
- last_30_days: "지난 30일"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "전체"
- neutral: "중립국"
- group_types:
- category: "카테고리"
- tag: "태그"
- table:
- total_count: "합계"
- summarization:
- chat:
- title: "메시지 요약"
- description: "선택된 시간 동안 전송된 대화 내역을 요약하기 위한 옵션을 선택하십시오."
- summarize: "요약하기"
- since:
- other: "약 %{count}시간"
- discobot_discoveries:
- regular_results: "글"
- collapse: "축소"
- tooltip:
- actions:
- disable: "비활성화"
diff --git a/config/locales/client.lt.yml b/config/locales/client.lt.yml
deleted file mode 100644
index e12fcf17..00000000
--- a/config/locales/client.lt.yml
+++ /dev/null
@@ -1,356 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-lt:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Leidžia dirbtinio intelekto paiešką"
- stream_completion: "Leidžia transliuoti dirbtinio intelekto personažų užbaigimus"
- update_personas: "Leidžia atnaujinti dirbtinio intelekto personas"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "Emocija"
- description: "Lentelėje pateikiamas įrašų, priskirtų konkrečiai emocijai, skaičius. Klasifikuota pagal modelį „SamLowe/roberta-base-go_emotions“."
- reports:
- filters:
- group_by:
- label: "Grupuoti pagal"
- sort_by:
- label: "Rūšiuoti pagal"
- tag:
- label: "Žymėti"
- logs:
- staff_actions:
- actions:
- create_ai_llm_model: "Sukurkite LLM modelį"
- update_ai_llm_model: "Atnaujinti LLM modelį"
- delete_ai_llm_model: "Ištrinti LLM modelį"
- create_ai_persona: "Sukurkite AI personažą"
- update_ai_persona: "Atnaujinti dirbtinio intelekto personažą"
- delete_ai_persona: "Ištrinti dirbtinio intelekto personažą"
- create_ai_tool: "Sukurkite dirbtinio intelekto įrankį"
- update_ai_tool: "Atnaujinti dirbtinio intelekto įrankį"
- delete_ai_tool: "Ištrinti dirbtinio intelekto įrankį"
- create_ai_embedding: "Sukurti dirbtinio intelekto įterpimą"
- update_ai_embedding: "Atnaujinti dirbtinio intelekto įterpimą"
- delete_ai_embedding: "Ištrinti dirbtinio intelekto įterpimą"
- update_ai_spam_settings: "Atnaujinti dirbtinio intelekto šlamšto nustatymus"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Siuntėjas"
- description: "Vartotojas, kuris atsiųs ataskaitą"
- receivers:
- label: "Gavėjai"
- description: "Vartotojai, kurie gaus ataskaitą (el. laiškai bus siunčiami tiesioginiais el. laiškais, naudotojų vardai bus išsiųsti PM)"
- topic_id:
- label: "Temos ID"
- description: "Temos ID, į kurį reikia skelbti ataskaitą"
- title:
- label: "Antraštė"
- description: "Pranešimo pavadinimas"
- days:
- label: "Dienos"
- description: "Ataskaitos laikotarpis"
- offset:
- label: "Poslinkis"
- description: "Testuodami ataskaitą galite paleisti senesne data, naudodami poslinkį, kad ataskaita būtų pradėta anksčiau."
- instructions:
- label: "Instrukcijos"
- description: "Didelio kalbos modelio instrukcijos"
- sample_size:
- label: "Pavyzdžio dydis"
- description: "Įrašų, į kuriuos reikia įtraukti ataskaitą, skaičius"
- tokens_per_post:
- label: "Žetonų už įrašą"
- description: "LLM žetonų, kuriuos galima naudoti vienam įrašui, skaičius"
- model:
- label: "Modelis"
- description: "LLM, naudojama ataskaitų generavimui"
- categories:
- label: "Kategorijos"
- description: "Filtruoti temas tik pagal šias kategorijas"
- tags:
- label: "Žymos"
- description: "Filtruoti temas tik pagal šias žymas"
- exclude_tags:
- label: "Išskirti žymas"
- description: "Išskirti temas su šiomis žymėmis"
- exclude_categories:
- label: "Išskirti kategorijas"
- description: "Išskirti temas su šiomis kategorijomis"
- allow_secure_categories:
- label: "Leisti saugias kategorijas"
- description: "Leisti generuoti ataskaitą temoms, esančioms saugiose kategorijose"
- suppress_notifications:
- label: "Slėpti pranešimus"
- description: "Neleisti ataskaitos generuojamų pranešimų, transformuojant juos į turinį. Tai pakeis paminėjimų ir vidinių nuorodų susiejimą."
- debug_mode:
- label: "Derinimo režimas"
- description: "Įjunkite derinimo režimą, kad matytumėte neapdorotus LLM įvesties ir išvesties duomenis"
- priority_group:
- label: "Pagrindinė Grupė"
- description: "Ataskaitoje teikti pirmenybę šios grupės turiniui"
- temperature:
- label: "Temperatūra"
- description: "Temperatūra, kurią naudosite LLM. Padidinkite, kad padidintumėte atsitiktinumą (palikite tuščią, kad naudotumėte numatytuosius modelio parametrus)."
- top_p:
- label: "Viršutinė P"
- description: "Viršutinė P, naudojama LLM, padidinkite, kad padidėtų atsitiktinumas (palikite tuščią, jei norite naudoti numatytuosius modelio parametrus)"
- llm_tool_triage:
- fields:
- model:
- label: "Modelis"
- description: "Numatytasis kalbos modelis, naudojamas triažui"
- tool:
- label: "Įrankis"
- description: "Įrankis, naudojamas triažui (įrankis negali turėti apibrėžtų parametrų)"
- llm_persona_triage:
- fields:
- persona:
- label: "Persona"
- llm_triage:
- fields:
- system_prompt:
- label: "Sistemos pranešimas"
- description: "Pranešimas, kuris bus naudojamas nustatant, įsitikinkite, kad jis turi atsakyti vienu žodžiu, kurį galite naudoti veiksmui suaktyvinti"
- search_for_text:
- label: "Ieškokite teksto"
- category:
- label: "Kategorija"
- description: "Kategorija, kuriai taikoma tema"
- tags:
- label: "Žymos"
- description: "Žymos, kuriai taikoma tema"
- canned_reply:
- label: "Atsakyti"
- canned_reply_user:
- label: "Atsakyti Vartotojas"
- description: "Vartotojo vardas, norintis paskelbti paruoštą atsakymą"
- hide_topic:
- label: "Slėpti temą"
- model:
- label: "Modelis"
- discourse_ai:
- title: "AI"
- features:
- back: "Atgal"
- disabled: "(uždrausta)"
- groups: "Grupės:"
- no_groups: "Nieko"
- edit: "Redaguoti"
- expand_list:
- one: "(%{count} daugiau)"
- few: "(%{count}daugiau)"
- many: "(%{count}daugiau)"
- other: "(%{count} daugiau)"
- collapse_list: "(rodyti mažiau)"
- filters:
- all: "Visos"
- reset: "Atstatyti"
- search:
- name: "Paieška"
- inference:
- match_concepts: "Sąvokų atitikimas"
- deduplicate_concepts: "Sąvokų deduplikacija"
- ai_helper:
- name: "Padėjėjas"
- description: "Padeda vartotojams bendrauti bendruomenėje, pavyzdžiui, kurti temas, rašyti įrašus ir skaityti turinį"
- proofread: Korektūruotas tekstas
- title_suggestions: "Siūlyti pavadinimus"
- explain: "Paaiškinkite"
- illustrate_post: "Iliustruoti įrašą"
- smart_dates: "Išmaniosios datos"
- translate: "Versti"
- markdown_tables: "Sukurti Markdown lentelę"
- custom_prompt: "Pasirinktinis raginimas"
- image_caption: "Antraštės vaizdai"
- translation:
- name: "Vertimai"
- description: "Verčia turinį į palaikomas kalbas"
- locale_detector: "Lokalės detektorius"
- post_raw_translator: "Neapdoroto įrašo vertėjas"
- topic_title_translator: "Temos pavadinimo vertėjas"
- short_text_translator: "Trumpų tekstų vertėjas"
- spam:
- name: "Šlamštas"
- description: "Naudodamas pasirinktą LLM, identifikuoja galimą šlamštą ir pažymi jį svetainės moderatoriams, kad jie galėtų jį patikrinti peržiūros eilėje."
- inspect_posts: "Apžiūrėti įrašus"
- modals:
- select_option: "Pasirink nustatymą..."
- layout:
- table: "Lentelė"
- card: "Kortelė"
- spam:
- short_title: "Šlamštas"
- title: "Šlamšto tvarkymo konfigūravimas"
- select_llm: "Pasirinkite teisės magistro laipsnį (LLM)"
- select_persona: "Pasirinkite asmenį"
- custom_instructions: "Individualūs nurodymai"
- custom_instructions_help: "Jūsų svetainei pritaikytos instrukcijos, padedančios dirbtiniam intelektui atpažinti šlamštą, pvz., „Aktyviau nuskaitykite įrašus ne anglų kalba“."
- last_seven_days: "Paskutinės 7 dienos"
- scanned_count: "Įrašai nuskaityti"
- false_positives: "Neteisingai pažymėta"
- false_negatives: "Praleistas šlamštas"
- spam_detected: "Aptiktas šlamštas"
- custom_instructions_placeholder: "Svetainėms skirtos instrukcijos dirbtiniam intelektui, padedančios tiksliau atpažinti šlamštą"
- enable: "Įgalinti"
- spam_tip: "Dirbtinio intelekto šlamšto aptikimo funkcija nuskaitys pirmuosius 3 visų naujų vartotojų įrašus viešose temose. Jie bus pažymėti peržiūrai ir užblokuoti vartotojus, jei jie greičiausiai yra šlamštas."
- settings_saved: "Nustatymai išsaugoti"
- spam_description: "Naudodamas pasirinktą LLM, identifikuoja galimą šlamštą ir pažymi jį svetainės moderatoriams, kad jie galėtų jį patikrinti peržiūros eilėje."
- no_llms: "Nėra laisvų teisės magistro laipsnių (LLM)."
- test_button: "Bandymas..."
- save_button: "Išsaugoti pakeitimus"
- test_modal:
- title: "Šlamšto aptikimo bandymas"
- post_url_label: "Įrašo URL arba ID"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 arba įrašo ID"
- result: "Rezultatas"
- scan_log: "Nuskaitymo žurnalas"
- spam: "Šlamštas"
- usage:
- summary: "Santrauka"
- model: "Modelis"
- username: "Vartotojo vardas"
- total_requests: "Visos užklausos"
- periods:
- last_day: "Paskutinės 24 valandos"
- custom: "Pasirinktinis..."
- ai_persona:
- back: "Atgal"
- name: "Vardas"
- edit: "Redaguoti"
- export: "Eksportuoti"
- description: "Aprašymas"
- user: Narys
- save: "Išsaugoti"
- enabled: "Galimas?"
- delete: "Pašalinti"
- response_format:
- open_modal: "Redaguoti"
- modal:
- key_title: "Raktas"
- filters:
- reset: "Atstatyti"
- rag:
- uploads:
- title: "Įkėlimai"
- uploading: "Įkeliama"
- tools:
- back: "Atgal"
- export: "Eksportuoti"
- name: "Vardas"
- description: "Aprašymas"
- summary: "Santrauka"
- save: "Išsaugoti"
- remove_parameter: "Pašalinti"
- parameter_required: "Privalomi"
- edit: "Redaguoti"
- delete: "Pašalinti"
- llms:
- display_name: "Vardas"
- save: "Išsaugoti"
- edit: "Redaguoti"
- back: "Atgal"
- delete: Pašalinti
- quotas:
- group: "Grupė"
- max_usages: "Maksimaliai naudoja"
- duration: "Trukmė"
- durations:
- hour: "1 valanda"
- six_hours: "6 valandos"
- day: "24 valandos"
- custom: "Pasirinktinis..."
- hours: "valandos"
- usage:
- ai_summarization: "Apibendrinti"
- ai_spam: "Šlamštas"
- next:
- title: "Kitas"
- tests:
- success: "Sėkmingai!"
- providers:
- google: "Google"
- fake: "Išskirtinės"
- ai_helper:
- context_menu:
- trigger: "Paklauskite AI"
- cancel: "Atšaukti"
- confirm: "Patvirtinti"
- discard: "Išmesti"
- post_options_menu:
- trigger: "Paklauskite AI"
- title: "Paklauskite AI"
- close: "Uždaryti"
- copy: "Kopijuoti"
- copied: "Nukopijuota!"
- cancel: "Atšaukti"
- image_caption:
- button_label: "Antraštė su AI"
- generating: "Generuojama antraštė..."
- credits: "Antraštė teikiama AI"
- save_caption: "Išsaugoti"
- automatic_caption_dialog:
- confirm: "Įgalinti"
- embeddings:
- back: "Atgal"
- save: "Išsaugoti"
- delete: "Pašalinti"
- edit: "Redaguoti"
- tests:
- success: "Sėkmingai!"
- display_name: "Vardas"
- providers:
- google: "Google"
- fake: "Išskirtinės"
- ai_bot:
- llm: "Modelis"
- debug_ai_modal:
- request: "Užklausa"
- response: "Atsakymas"
- next_log: "Kitas"
- previous_log: "Buvęs"
- invite_ai_conversation:
- button: "Kviesti"
- ai_label: "AI"
- share_modal:
- copy: "Kopijuoti"
- bot_names:
- claude-2: "Claude 2"
- conversations:
- today: "Šiandien"
- last_7_days: "Paskutinės 7 dienos"
- last_30_days: "Paskutinės 30 dienų"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Visos"
- neutral: "Neutrali"
- group_types:
- category: "Kategorija"
- tag: "Žymėti"
- table:
- total_count: "Viso"
- summarization:
- chat:
- summarize: "Apibendrinti"
- discobot_discoveries:
- regular_results: "Temos"
- collapse: "Suskleisti"
- tooltip:
- actions:
- disable: "Išjungti"
diff --git a/config/locales/client.lv.yml b/config/locales/client.lv.yml
deleted file mode 100644
index a42fce33..00000000
--- a/config/locales/client.lv.yml
+++ /dev/null
@@ -1,165 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-lv:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Kārtot pēc"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Tēmas ID"
- title:
- label: "Virsraksts"
- categories:
- label: "Sadaļas"
- tags:
- label: "Birkas"
- llm_triage:
- fields:
- category:
- label: "Sadaļa"
- tags:
- label: "Birkas"
- canned_reply:
- label: "Atbilde"
- discourse_ai:
- features:
- back: "Atpakaļ"
- disabled: "(atslēgt)"
- groups: "Grupas:"
- no_groups: "Nav"
- edit: "Rediģēt"
- expand_list:
- zero: "(vēl %{count})"
- one: "(vēl %{count})"
- other: "(vēl %{count})"
- filters:
- all: "Viss"
- reset: "Atlikt"
- search:
- name: "Meklēt"
- spam:
- name: "Spams"
- modals:
- select_option: "Izvēlieties opciju..."
- spam:
- short_title: "Spams"
- last_seven_days: "Pēdējās 7 dienas"
- enable: "Ieslēgt"
- test_modal:
- spam: "Spams"
- usage:
- summary: "Kopsavilkums"
- username: "Lietotājvārds"
- periods:
- last_day: "Pēdējās 24 stundas"
- ai_persona:
- back: "Atpakaļ"
- name: "Vārds"
- edit: "Rediģēt"
- export: "Eksportēt"
- description: "Apraksts"
- user: Lietotājs
- save: "Saglabāt"
- enabled: "Ieslēgts?"
- delete: "Dzēst"
- response_format:
- open_modal: "Rediģēt"
- filters:
- reset: "Atlikt"
- rag:
- uploads:
- title: "Augšupielādes"
- uploading: "Notiek ielāde..."
- tools:
- back: "Atpakaļ"
- export: "Eksportēt"
- name: "Vārds"
- description: "Apraksts"
- summary: "Kopsavilkums"
- save: "Saglabāt"
- remove_parameter: "Atcelt"
- parameter_required: "Nepieciešams"
- edit: "Rediģēt"
- delete: "Dzēst"
- llms:
- display_name: "Vārds"
- save: "Saglabāt"
- edit: "Rediģēt"
- back: "Atpakaļ"
- delete: Dzēst
- quotas:
- group: "Grupa"
- max_usages: "Maksimāli izmantojams"
- duration: "Ilgums"
- hours: "stunda"
- usage:
- ai_spam: "Spams"
- next:
- title: "Nākamais"
- tests:
- success: "Veiksmīgi!"
- providers:
- google: "Google"
- ai_helper:
- context_menu:
- cancel: "Atcelt"
- discard: "Izmest"
- post_options_menu:
- close: "Aizvērt"
- copy: "Kopēt"
- copied: "Nokopēts!"
- cancel: "Atcelt"
- image_caption:
- save_caption: "Saglabāt"
- automatic_caption_dialog:
- confirm: "Ieslēgt"
- embeddings:
- back: "Atpakaļ"
- save: "Saglabāt"
- delete: "Dzēst"
- edit: "Rediģēt"
- tests:
- success: "Veiksmīgi!"
- display_name: "Vārds"
- providers:
- google: "Google"
- ai_bot:
- debug_ai_modal:
- request: "Pieprasījums"
- response: "Atbilde"
- next_log: "Nākamais"
- previous_log: "Iepriekšējais"
- invite_ai_conversation:
- button: "Ielūgt"
- share_modal:
- copy: "Kopēt"
- conversations:
- today: "Šodien"
- last_7_days: "Pēdējās 7 dienas"
- last_30_days: "Pēdējās 30 dienas"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Viss"
- group_types:
- category: "Sadaļa"
- table:
- total_count: "Kopā"
- discobot_discoveries:
- regular_results: "Tēmas"
- collapse: "Sakļaut"
- tooltip:
- actions:
- disable: "Atslēgt"
diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml
deleted file mode 100644
index 5267bb74..00000000
--- a/config/locales/client.nb_NO.yml
+++ /dev/null
@@ -1,175 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-nb_NO:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Sorter etter"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Emne-ID"
- title:
- label: "Tittel"
- categories:
- label: "Kategorier"
- tags:
- label: "Stikkord"
- llm_triage:
- fields:
- category:
- label: "Kategori"
- tags:
- label: "Stikkord"
- canned_reply:
- label: "Svar"
- discourse_ai:
- features:
- back: "Forrige"
- disabled: "(deaktivert)"
- groups: "Grupper:"
- no_persona: "Ikke angitt"
- no_groups: "Ingen"
- edit: "Endre"
- expand_list:
- one: "(%{count} mer)"
- other: "(%{count} mer)"
- collapse_list: "(vis mindre)"
- filters:
- all: "Alle"
- reset: "Tilbakestill"
- search:
- name: "Søk"
- spam:
- name: "Spam"
- modals:
- select_option: "Velg et alternativ..."
- spam:
- short_title: "Spam"
- last_seven_days: "Siste 7 dager"
- enable: "Aktiver"
- test_modal:
- spam: "Spam"
- usage:
- summary: "Sammendrag"
- username: "Brukernavn"
- total_requests: "Totalt forespørsler"
- periods:
- last_day: "Siste 24 timer"
- ai_persona:
- back: "Forrige"
- name: "Navn"
- edit: "Endre"
- export: "Eksporter"
- description: "Beskrivelse"
- user: Bruker
- save: "Lagre"
- enabled: "Aktivert?"
- delete: "Slett"
- response_format:
- open_modal: "Endre"
- filters:
- reset: "Tilbakestill"
- rag:
- uploads:
- title: "Opplastinger"
- uploading: "Laster opp…"
- tools:
- back: "Forrige"
- export: "Eksporter"
- name: "Navn"
- description: "Beskrivelse"
- summary: "Sammendrag"
- save: "Lagre"
- remove_parameter: "Fjern"
- parameter_required: "Påkrevd"
- edit: "Endre"
- delete: "Slett"
- llms:
- display_name: "Navn"
- save: "Lagre"
- edit: "Endre"
- back: "Forrige"
- delete: Slett
- quotas:
- group: "Gruppe"
- max_usages: "Maks antall bruk"
- duration: "Varighet"
- durations:
- hour: "1 time"
- six_hours: "6 timer"
- day: "24 timer"
- week: "7 dager"
- hours: "timer"
- usage:
- ai_summarization: "Oppsummer"
- ai_spam: "Spam"
- next:
- title: "Neste"
- tests:
- success: "Suksess!"
- providers:
- google: "Google"
- fake: "Egendefinert"
- ai_helper:
- context_menu:
- cancel: "Avbryt"
- discard: "Forkast"
- post_options_menu:
- close: "Lukk"
- copy: "Kopier"
- copied: "Kopiert!"
- cancel: "Avbryt"
- image_caption:
- save_caption: "Lagre"
- automatic_caption_dialog:
- confirm: "Aktiver"
- embeddings:
- back: "Forrige"
- save: "Lagre"
- delete: "Slett"
- edit: "Endre"
- tests:
- success: "Suksess!"
- display_name: "Navn"
- providers:
- google: "Google"
- fake: "Egendefinert"
- ai_bot:
- debug_ai_modal:
- request: "Forespørsel"
- response: "Svar"
- next_log: "Neste"
- previous_log: "Forrige"
- invite_ai_conversation:
- button: "Inviter"
- share_modal:
- copy: "Kopier"
- conversations:
- today: "I dag"
- last_7_days: "Siste 7 dager"
- last_30_days: "Siste 30 dager"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Alle"
- group_types:
- category: "Kategori"
- table:
- total_count: "Total"
- discobot_discoveries:
- regular_results: "Emner"
- collapse: "Fold sammen"
- tooltip:
- actions:
- disable: "Deaktiver"
diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml
deleted file mode 100644
index 75540759..00000000
--- a/config/locales/client.nl.yml
+++ /dev/null
@@ -1,686 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-nl:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Staat AI zoeken toe"
- stream_completion: "Maakt streamen van voltooiingen van AI persona's mogelijk"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "Emotie"
- description: "De tabel bevat een telling van berichten die zijn geclassificeerd met een bepaalde emotie. Geclassificeerd met het model 'SamLowe/roberta-base-go_emotions'."
- reports:
- filters:
- sort_by:
- label: "Sorteren op"
- tag:
- label: "Taggen"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Afzender"
- description: "De gebruiker die het rapport verzendt"
- receivers:
- label: "Ontvangers"
- description: "De gebruikers die het rapport ontvangen (e-mails worden rechtstreeks verzonden, gebruikersnamen ontvangen een PB)"
- topic_id:
- label: "Topic-ID"
- description: "De ID van het topic waarin het rapport moet worden geplaatst"
- title:
- label: "Titel"
- description: "De titel van het rapport"
- days:
- label: "Dagen"
- description: "Het tijdsbestek van het rapport"
- offset:
- label: "Offset"
- description: "Bij het testen wil je het rapport wellicht historisch uitvoeren. Gebruik offset om het rapport op een eerdere datum te starten"
- instructions:
- label: "Instructies"
- description: "De instructies voor het grote taalmodel"
- sample_size:
- label: "Monstergrootte"
- description: "Het aantal berichten om te gebruiken voor het rapport"
- tokens_per_post:
- label: "Tokens per bericht"
- description: "Het aantal LLM-tokens dat per bericht moet worden gebruikt"
- model:
- label: "Model"
- description: "Te gebruiken LLM voor het genereren van het rapport"
- categories:
- label: "Categorieën"
- description: "Filter topics alleen op deze categorieën"
- tags:
- label: "Tags"
- description: "Filter topics alleen op deze tags"
- exclude_tags:
- label: "Tags uitsluiten"
- description: "Sluit topics met deze tags uit"
- exclude_categories:
- label: "Categorieën uitsluiten"
- description: "Sluit topics met deze categorieën uit"
- allow_secure_categories:
- label: "Beveiligde categorieën toestaan"
- description: "Sta toe dat het rapport wordt gegenereerd voor topics in beveiligde categorieën"
- suppress_notifications:
- label: "Meldingen onderdrukken"
- description: "Onderdruk meldingen die het rapport kan genereren door ze om te zetten in inhoud. Hierdoor worden vermeldingen en interne links opnieuw toegewezen."
- debug_mode:
- label: "Foutopsporingsmodus"
- description: "Schakel de foutopsporingsmodus in om de onbewerkte in- en uitvoer van het LLM te bekijken"
- priority_group:
- label: "Prioriteitsgroep"
- description: "Geef prioriteit aan content van deze groep in het rapport"
- temperature:
- label: "Temperatuur"
- top_p:
- label: "Top P"
- llm_tool_triage:
- fields:
- model:
- label: "Model"
- llm_triage:
- fields:
- system_prompt:
- label: "Systeemprompt"
- description: "De prompt die wordt gebruikt voor triage. Antwoord met een enkel woord dat kan worden gebruikt om de actie te activeren"
- max_post_tokens:
- label: "Maximaal aantal plaatsingstokens"
- description: "Het maximale aantal tokens om te scannen met LLM-triage"
- stop_sequences:
- label: "Stopsequenties"
- description: "Instrueer het model om het genereren van tokens te stoppen als een van deze waarden wordt bereikt"
- search_for_text:
- label: "Tekst zoeken"
- description: "Pas deze acties toe als de volgende tekst voorkomt in het LLM-antwoord"
- category:
- label: "Categorie"
- description: "Toe te passen categorie op het topic"
- tags:
- label: "Tags"
- description: "Toe te passen tags op het topic"
- canned_reply:
- label: "Antwoorden"
- description: "Ruwe tekst van standaard antwoord te plaatsen in het topic"
- canned_reply_user:
- label: "Antwoordgebruiker"
- description: "Gebruikersnaam van de gebruiker die het standaardantwoord plaatst"
- hide_topic:
- label: "Topic verbergen"
- description: "Maak het topic onzichtbaar voor het publiek als dit wordt geactiveerd"
- flag_type:
- label: "Markeringstype"
- description: "Type markering om toe te passen op het bericht (spam of gewoon verhogen voor beoordeling)"
- flag_post:
- label: "Bericht markeren"
- description: "Markeert berichten (als spam of ter beoordeling)"
- include_personal_messages:
- label: "Persoonlijke berichten opnemen"
- description: "Scan en triageer ook persoonlijke berichten"
- model:
- label: "Model"
- description: "Taalmodel gebruikt voor triage"
- temperature:
- label: "Temperatuur"
- discourse_ai:
- title: "AI"
- features:
- back: "Terug"
- disabled: "(uitgeschakeld)"
- groups: "Groepen:"
- no_persona: "Niet ingesteld"
- no_groups: "Geen"
- edit: "Bewerken"
- expand_list:
- one: "(Nog %{count})"
- other: "(Nog %{count})"
- collapse_list: "(minder weergeven)"
- filters:
- all: "Alles"
- reset: "Resetten"
- search:
- name: "Zoeken"
- embeddings:
- name: "Insluitingen"
- ai_helper:
- name: "Helper"
- proofread: Tekst proeflezen
- explain: "Uitleggen"
- smart_dates: "Slimme datums"
- markdown_tables: "Markdowntabel genereren"
- custom_prompt: "Aangepaste prompt"
- spam:
- name: "Spam"
- description: "Identificeert potentiële spam met behulp van de geselecteerde LLM en markeert deze voor sitemoderators om te inspecteren in de beoordelingswachtrij"
- modals:
- select_option: "Selecteer een optie..."
- spam:
- short_title: "Spam"
- title: "Spamafhandeling configureren"
- select_llm: "Selecteer LLM"
- custom_instructions: "Aangepaste instructies"
- custom_instructions_help: "Aangepaste instructies specifiek voor jouw site om de AI te helpen bij het identificeren van spam, bijvoorbeeld 'Wees agressiever bij het scannen van berichten die niet in het Engels zijn'."
- last_seven_days: "Afgelopen 7 dagen"
- scanned_count: "Gescande berichten"
- false_positives: "Onjuist gemarkeerd"
- false_negatives: "Gemiste spam"
- spam_detected: "Gedetecteerde spam"
- custom_instructions_placeholder: "Sitespecifieke instructies voor de AI om spam nauwkeuriger te identificeren"
- enable: "Inschakelen"
- spam_tip: "AI-spamdetectie scant de eerste 3 berichten van alle nieuwe gebruikers bij openbare topics. De AI markeert ze voor beoordeling en blokkeert gebruikers als ze waarschijnlijk spam zijn."
- settings_saved: "Instellingen opgeslagen"
- spam_description: "Identificeert potentiële spam met behulp van de geselecteerde LLM en markeert deze voor sitemoderators om te inspecteren in de beoordelingswachtrij"
- no_llms: "Geen LLM's beschikbaar"
- test_button: "Testen..."
- save_button: "Wijzigingen opslaan"
- test_modal:
- title: "Spamdetectie testen"
- post_url_label: "Bericht-URL of -ID"
- post_url_placeholder: "https://jouw-forum.com/t/topic/123/4 of bericht-ID"
- result: "Resultaat"
- scan_log: "Scanlog"
- run: "Test uitvoeren"
- spam: "Spam"
- not_spam: "Geen spam"
- stat_tooltips:
- incorrectly_flagged: "Items die de AI-bot als spam heeft gemarkeerd en waar de moderators het niet mee eens waren"
- missed_spam: "Door de community als spam gemarkeerde items die niet door de AI-bot zijn gedetecteerd en waar de moderators het mee eens waren"
- errors:
- scan_not_admin:
- message: "Waarschuwing: het scannen van spam werkt niet goed omdat het account voor het scannen van spam geen beheerder is."
- action: "Oplossen"
- resolved: "De fout is opgelost!"
- usage:
- short_title: "Gebruik"
- summary: "Samenvatting"
- total_tokens: "Totaal aantal tokens"
- tokens_over_time: "Tokens in de loop van de tijd"
- features_breakdown: "Gebruik per functie"
- feature: "Functie"
- usage_count: "Gebruiksaantal"
- model: "Model"
- models_breakdown: "Gebruik per model"
- users_breakdown: "Gebruik per gebruiker"
- all_features: "Alle functies"
- all_models: "Alle modellen"
- username: "Gebruikersnaam"
- total_requests: "Totaal aantal verzoeken"
- request_tokens: "Verzoektokens"
- response_tokens: "Antwoordtokens"
- net_request_tokens: "Netto verzoektokens"
- cached_tokens: "Gecachete tokens"
- cached_request_tokens: "Gecachete verzoektokens"
- no_users: "Geen gebruikergebruiksgegevens gevonden"
- no_models: "Geen modelgebruiksgegevens gevonden"
- no_features: "Geen functiegebruiksgegevens gevonden"
- subheader_description: "Tokens zijn de basiseenheden die LLM's gebruiken om tekst te begrijpen en te genereren. Gebruiksgegevens kunnen de kosten beïnvloeden"
- stat_tooltips:
- total_requests: "Alle verzoeken aan LLM's via Discourse"
- total_tokens: "Alle tokens die zijn gebruikt bij het prompten van een LLM"
- request_tokens: "Tokens die zijn gebruikt wanneer de LLM probeert te begrijpen wat je zegt"
- response_tokens: "Tokens die zijn gebruikt wanneer de LLM antwoordt op je prompt"
- cached_tokens: "Eerder verwerkte verzoektokens die de LLM hergebruikt om prestaties en kosten te optimaliseren"
- periods:
- last_day: "Afgelopen 24 uur"
- last_week: "Vorige week"
- last_month: "Vorige maand"
- custom: "Aangepast..."
- ai_persona:
- ai_tools: "Tools"
- tool_strategies:
- all: "Toepassen op alle antwoorden"
- replies:
- one: "Alleen toepassen op eerste antwoord"
- other: "Alleen toepassen op eerste %{count} antwoorden"
- back: "Vorige"
- name: "Naam"
- edit: "Bewerken"
- export: "Exporteren"
- description: "Beschrijving"
- no_llm_selected: "Geen taalmodel geselecteerd"
- max_context_posts: "Maximaal aantal contextberichten"
- max_context_posts_help: "Het maximale aantal berichten dat als context voor de AI kan worden gebruikt bij het reageren op een gebruiker. (Leeg voor standaardinstelling)"
- vision_enabled: Zicht ingeschakeld
- vision_enabled_help: Indien ingeschakeld, zal de AI proberen afbeeldingen te begrijpen die gebruikers in het topic plaatsen, afhankelijk van het gebruikte model ter ondersteuning van zicht. Ondersteund door de nieuwste modellen van Anthropic, Google en OpenAI.
- vision_max_pixels: Ondersteunde afbeeldingsgrootte
- vision_max_pixel_sizes:
- low: Lage kwaliteit - goedkoopst (256x256)
- medium: Middelmatige kwaliteit (512x512)
- high: Hoge kwaliteit - langzaamst (1024x1024)
- tool_details: Tooldetails weergeven
- tool_details_help: Toont eindgebruikers informatie over welke tools het taalmodel heeft geactiveerd.
- mentionable: Vermeldingen toestaan
- mentionable_help: Indien ingeschakeld, kunnen gebruikers in toegestane groepen deze gebruiker vermelden in berichten. De AI reageert als deze persona.
- user: Gebruiker
- create_user: Gebruiker maken
- create_user_help: Je kunt optioneel een gebruiker aan deze persona koppelen. Als je dat doet, gebruikt de AI deze gebruiker om op verzoeken te antwoorden.
- default_llm: Standaard taalmodel
- default_llm_help: Het standaard taalmodel dat voor deze persona moet worden gebruikt. Vereist als je de persona wilt vermelden in openbare berichten.
- question_consolidator_llm: Taalmodel voor Vragenconsolidator
- question_consolidator_llm_help: Het te gebruiken taalmodel voor de vragenconsolidator. Je kunt een minder krachtig model kiezen om kosten te besparen.
- system_prompt: Systeemprompt
- forced_tool_strategy: Gedwongen toolstrategie
- allow_chat_direct_messages: "Directe chatberichten toestaan"
- allow_chat_direct_messages_help: "Indien ingeschakeld, kunnen gebruikers in toegestane groepen directe berichten naar deze persona sturen."
- allow_chat_channel_mentions: "Vermelding in chatkanalen toestaan"
- allow_chat_channel_mentions_help: "Indien ingeschakeld, kunnen gebruikers in toegestane groepen deze persona vermelden in chatkanalen."
- allow_personal_messages: "Persoonlijke berichten toestaan"
- allow_personal_messages_help: "Indien ingeschakeld, kunnen gebruikers in toegestane groepen persoonlijke berichten naar deze persona sturen."
- allow_topic_mentions: "Vermelding in topics toestaan"
- allow_topic_mentions_help: "Indien ingeschakeld, kunnen gebruikers in toegestane groepen deze persona vermelden in topics."
- force_default_llm: "Altijd standaard taalmodel gebruiken"
- save: "Opslaan"
- saved: "Persona opgeslagen"
- enabled: "Ingeschakeld?"
- tools: "Ingeschakelde tools"
- forced_tools: "Geforceerde tools"
- allowed_groups: "Toegestane groepen"
- confirm_delete: "Weet je zeker dat je deze persona wilt verwijderen?"
- new: "Nieuwe persona"
- no_personas: "Je hebt nog geen persona's gemaakt"
- title: "Persona's"
- short_title: "Persona's"
- delete: "Verwijderen"
- temperature: "Temperatuur"
- temperature_help: "Te gebruiken temperatuur voor de LLM. Verhoog deze om de creativiteit te vergroten (laat dit leeg om de standaardinstelling van het model te gebruiken, doorgaans een waarde van 0,0 tot 2,0)"
- top_p: "Top P"
- top_p_help: "Te gebruiken Top P voor de LLM. Verhoog deze om de willekeurigheid te vergroten (laat dit leeg om de standaardinstelling van het model te gebruiken, doorgaans een waarde van 0,0 tot 2,0)"
- priority: "Prioriteit"
- priority_help: "Prioritaire persona's worden bovenaan de personalijst weergegeven voor gebruikers. Als meerdere persona's prioriteit hebben, worden deze alfabetisch gesorteerd."
- tool_options: "Toolopties"
- rag_conversation_chunks: "Conversatiechunks zoeken"
- rag_conversation_chunks_help: "Het te gebruiken aantal chunks voor zoeken door het RAG-model. Verhoog dit om de hoeveelheid context te vergroten die de AI kan gebruiken."
- persona_description: "Persona's zijn een krachtige functie waarmee je het gedrag van de AI-engine kunt aanpassen in je Discourse-forum. Ze fungeren als een 'systeembericht' dat de reacties en interacties van de AI stuurt, waardoor je een persoonlijkere en boeiendere gebruikerservaring creëert."
- response_format:
- open_modal: "Bewerken"
- modal:
- key_title: "Sleutel"
- filters:
- reset: "Resetten"
- rag:
- options:
- rag_chunk_tokens: "Uploadchunktokens"
- rag_chunk_tokens_help: "Het te gebruiken aantal tokens voor elke chunk in het RAG-model. Verhoog dit om de hoeveelheid context die de AI kan gebruiken te vergroten. (Als je dit wijzigt, worden alle uploads opnieuw geïndexeerd)"
- rag_chunk_overlap_tokens: "Uploadchunkoverlaptokens"
- rag_chunk_overlap_tokens_help: "Het te overlappen aantal tokens tussen chunks in het RAG-model. (Als je dit wijzigt, worden alle uploads opnieuw geïndexeerd)"
- show_indexing_options: "Uploadopties weergeven"
- hide_indexing_options: "Uploadopties verbergen"
- uploads:
- title: "Uploads"
- button: "Bestanden toevoegen"
- filter: "Uploads filteren"
- indexed: "Geïndexeerd"
- indexing: "Indexeren"
- uploaded: "Klaar om te indexeren"
- uploading: "Uploaden..."
- remove: "Upload verwijderen"
- tools:
- back: "Vorige"
- short_title: "Tools"
- export: "Exporteren"
- no_tools: "Je hebt nog geen tools gemaakt"
- name: "Naam"
- new: "Nieuwe tool"
- description: "Beschrijving"
- description_help: "Een duidelijke beschrijving van het doel van de tool voor het taalmodel"
- subheader_description: "Tools breiden de mogelijkheden van AI-bots uit met door de gebruiker gedefinieerde JavaScript-functies."
- summary: "Samenvatting"
- summary_help: "Samenvatting van de tools die voor eindgebruikers moeten worden weergegeven"
- script: "Script"
- parameters: "Parameters"
- save: "Opslaan"
- remove_parameter: "Verwijderen"
- parameter_required: "Vereist"
- parameter_enum: "Enum"
- parameter_name: "Parameternaam"
- parameter_description: "Parameterbeschrijving"
- enum_value: "Enumwaarde"
- add_enum_value: "Enumwaarde toevoegen"
- edit: "Bewerken"
- test: "Test uitvoeren"
- delete: "Verwijderen"
- saved: "Tool opgeslagen"
- confirm_delete: "Weet je zeker dat je deze tool wilt verwijderen?"
- test_modal:
- title: "AI-tool testen"
- run: "Test uitvoeren"
- result: "Testresultaat"
- llms:
- short_title: "LLM's"
- no_llms: "Nog geen LLM's"
- new: "Nieuw model"
- display_name: "Naam"
- name: "Model-ID"
- provider: "Provider"
- tokenizer: "Tokenizer"
- url: "URL van de service die het model host"
- api_key: "API-sleutel van de service die het model host"
- enabled_chat_bot: "AI-botkiezer toestaan"
- vision_enabled: "Zicht ingeschakeld"
- ai_bot_user: "AI-botgebruiker"
- save: "Opslaan"
- edit: "Bewerken"
- saved: "LLM-model opgeslagen"
- back: "Vorige"
- confirm_delete: Weet je zeker dat je dit model wilt verwijderen?
- delete: Verwijderen
- seeded_warning: "Dit model is vooraf geconfigureerd op je site en kan niet worden bewerkt."
- quotas:
- title: "Gebruiksquota's"
- add_title: "Nieuw quotum maken"
- group: "Groep"
- max_tokens: "Maximaal aantal tokens"
- max_usages: "Max. gebruiken"
- duration: "Duur"
- confirm_delete: "Weet je zeker dat je dit quotum wilt verwijderen?"
- add: "Quotum toevoegen"
- durations:
- hour: "1 uur"
- six_hours: "6 uur"
- day: "24 uur"
- week: "7 dagen"
- custom: "Aangepast..."
- hours: "uur"
- max_tokens_help: "Maximum aantal tokens (woorden en tekens) dat elke gebruiker in deze groep kan gebruiken binnen de opgegeven tijdsduur. Tokens zijn de eenheden die door AI-modellen worden gebruikt om tekst te verwerken. Ruwweg 1 token = 4 tekens of 3/4 van een woord."
- max_usages_help: "Maximum aantal keren dat elke gebruiker in deze groep het AI-model kan gebruiken binnen de opgegeven tijdsduur. Dit quotum wordt bijgehouden per individuele gebruiker, niet gedeeld door de groep."
- usage:
- ai_bot: "AI-bot"
- ai_helper: "Helper"
- ai_persona: "Persona (%{persona})"
- ai_summarization: "Samenvatten"
- ai_embeddings_semantic_search: "AI zoeken"
- ai_spam: "Spam"
- in_use_warning:
- one: "Dit model wordt momenteel gebruikt door %{settings}. Als het onjuist is geconfigureerd, werkt de functie niet zoals verwacht."
- other: "Dit model wordt momenteel gebruikt door de volgende: %{settings}. Als het onjuist is geconfigureerd, werken functies niet zoals verwacht. "
- model_description:
- none: "Algemene instellingen die werken voor de meeste taalmodellen"
- anthropic-claude-opus-4-0: "Het intelligentste model van Antropic"
- anthropic-claude-3-5-haiku-latest: "Snel en kosteneffectief"
- google-gemini-2-5-flash: "Lichtgewicht, snel en kostenefficiënt met multimodale redenering"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Efficiënt lichtgewicht meertalig model"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Krachtig multifunctioneel model"
- mistral-mistral-large-latest: "Het krachtigste model van Mistral"
- mistral-pixtral-large-latest: "Het krachtigste model met zichtcapaciteit van Mistral"
- preseeded_model_description: "Voorgeconfigureerd open-source model met %{model}"
- configured:
- title: "Geconfigureerde LLM's"
- preconfigured_llms: "Selecteer je LLM"
- preconfigured:
- title_no_llms: "Selecteer een sjabloon om te beginnen"
- title: "Niet-geconfigureerde LLM-sjablonen"
- description: "LLM's (Large Language Models) zijn AI-tools die zijn geoptimaliseerd voor taken als het samenvatten van inhoud, het genereren van rapporten, het automatiseren van klantinteracties en het faciliteren van forummoderatie en inzichten"
- fake: "Handmatige configuratie"
- button: "Instellen"
- next:
- title: "Volgende"
- tests:
- title: "Test uitvoeren"
- running: "Test uitvoeren..."
- success: "Succes!"
- failure: "Een poging om verbinding te maken met het model resulteerde in deze fout: %{error}"
- hints:
- name: "We nemen dit op in de API-aanroep om aan te geven welk model we gebruiken"
- vision_enabled: "Indien ingeschakeld, zal de AI proberen afbeeldingen te begrijpen. Dit is afhankelijk van het gebruikte model ter ondersteuning van zicht. Ondersteund door de nieuwste modellen van Anthropic, Google en OpenAI."
- enabled_chat_bot: "Indien ingeschakeld, kunnen gebruikers dit model selecteren bij het maken van PB's met de AI-bot."
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Aangepast"
- provider_fields:
- access_key_id: "Toegangssleutel-ID AWS Bedrock"
- region: "Regio AWS Bedrock"
- organization: "Optionele organisatie-ID OpenAI"
- disable_system_prompt: "Systeembericht uitschakelen in prompts"
- enable_native_tool: "Ondersteuning voor native tools inschakelen"
- disable_native_tools: "Ondersteuning voor native tool uitschakelen (gebruik tools op XML-basis)"
- provider_order: "Volgorde van providers (door komma's gescheiden lijst)"
- provider_quantizations: "Volgorde van providerkwantificeringen (door komma's gescheiden lijst, bijvoorbeeld: fp16, fp8)"
- disable_streaming: "Streamingvoltooiingen uitschakelen (streaming- naar niet-streaming-verzoeken converteren)"
- related_topics:
- title: "Gerelateerde topics"
- pill: "Gerelateerd"
- ai_helper:
- title: "Stel wijzigingen voor met behulp van AI"
- description: "Kies een van de onderstaande opties, dan zal de AI je een nieuwe versie van de tekst voorstellen."
- selection_hint: "Tip: je kunt ook een deel van de tekst selecteren voordat je de hulp opent om alleen dat deel te herschrijven."
- suggest: "Voorstellen met AI"
- suggest_errors:
- too_many_tags:
- one: "Je kunt maximaal %{count} tag hebben"
- other: "Je kunt maximaal %{count} tags hebben"
- no_suggestions: "Geen suggesties beschikbaar"
- missing_content: "Voer wat inhoud in om suggesties te genereren."
- context_menu:
- trigger: "AI vragen"
- loading: "AI is aan het genereren"
- cancel: "Annuleren"
- confirm: "Bevestigen"
- discard: "Weggooien"
- changes: "Voorgestelde bewerkingen"
- custom_prompt:
- title: "Aangepaste prompt"
- placeholder: "Voer een aangepaste prompt in..."
- submit: "Prompt verzenden"
- translate_prompt: "Vertalen naar %{language}"
- post_options_menu:
- trigger: "AI vragen"
- title: "AI vragen"
- loading: "AI is aan het genereren"
- close: "Sluiten"
- copy: "Kopiëren"
- copied: "Gekopieerd!"
- cancel: "Annuleren"
- insert_footnote: "Voetnoot toevoegen"
- footnote_disabled: "Automatisch invoegen uitgeschakeld, klik op de knop Kopiëren en bewerk het handmatig"
- footnote_credits: "Uitleg door AI"
- fast_edit:
- suggest_button: "Bewerking voorstellen"
- thumbnail_suggestions:
- title: "Voorgestelde miniaturen"
- select: "Selecteren"
- selected: "Geselecteerd"
- image_caption:
- button_label: "Bijschrift met AI"
- generating: "Bijschrift genereren..."
- credits: "Bijschrift door AI"
- save_caption: "Opslaan"
- automatic_caption_setting: "Automatische bijschriften inschakelen"
- automatic_caption_loading: "Afbeeldingsbijschriften genereren..."
- automatic_caption_dialog:
- prompt: "Dit bericht bevat afbeeldingen zonder bijschrift. Wil je automatische bijschriften inschakelen bij het uploaden van afbeeldingen? (Je kunt dit later wijzigen in je voorkeuren)"
- confirm: "Inschakelen"
- cancel: "Niet meer vragen"
- no_content_error: "Voeg eerst inhoud toe om er AI-acties op uit te voeren"
- reviewables:
- model_used: "Gebruikt model:"
- accuracy: "Nauwkeurigheid:"
- embeddings:
- short_title: "Insluitingen"
- new: "Nieuwe insluiting"
- back: "Terug"
- save: "Opslaan"
- saved: "Insluitingsconfiguratie opgeslagen"
- delete: "Verwijderen"
- confirm_delete: Weet je zeker dat je deze insluitingsconfiguratie wilt verwijderen?
- empty: "Je hebt nog geen insluitingen ingesteld"
- presets: "Selecteer een preset..."
- configure_manually: "Handmatig configureren"
- edit: "Bewerken"
- seeded_warning: "Dit is vooraf geconfigureerd op je site en kan niet worden bewerkt."
- tests:
- title: "Test uitvoeren"
- running: "Test uitvoeren..."
- success: "Succes!"
- failure: "Pogingen om een insluitingen te genereren resulteerden in: %{error}"
- hints:
- dimensions_warning: "Eenmaal opgeslagen kan deze waarde niet meer worden gewijzigd."
- matryoshka_dimensions: "Bepaalt de grootte van geneste insluitingen die worden gebruikt voor hiërarchische of meerlaagse weergave van gegevens, vergelijkbaar met hoe geneste poppetjes in elkaar passen."
- sequence_length: "Het maximale aantal tokens dat in één keer kan worden verwerkt bij het maken van insluitingen of het verwerken van een query."
- distance_function: "Bepaalt hoe de mate van overeenkomst tussen insluitingen wordt berekend, met behulp van cosinusafstand (meting van de hoek tussen vectoren) of negatief binnenproduct (meting van de overlap van vectorwaarden)."
- display_name: "Naam"
- provider: "Provider"
- url: "URL insluitingsservice"
- api_key: "API-sleutel insluitingsservice"
- tokenizer: "Tokenizer"
- dimensions: "Insluitingsafmetingen"
- max_sequence_length: "Sequentielengte"
- embed_prompt: "Insluitingsprompt"
- search_prompt: "Zoekprompt"
- matryoshka_dimensions: "Matroesjka-afmetingen"
- distance_function: "Afstandsfunctie"
- distance_functions:
- "<#>": "Negatief binnenproduct"
- <=>: "Cosinusafstand"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Aangepast"
- provider_fields:
- model_name: "Modelnaam"
- semantic_search: "Topics (semantisch)"
- semantic_search_loading: "Zoeken naar meer resultaten met behulp van AI"
- semantic_search_results:
- toggle: "%{count} resultaten gevonden met AI worden weergegeven"
- toggle_hidden: "%{count} resultaten gevonden met AI zijn verborgen"
- none: "Sorry, AI zoeken heeft geen overeenkomende topics gevonden"
- new: "Druk op 'Zoeken' om nieuwe resultaten te zoeken met AI"
- unavailable: "AI-resultaten niet beschikbaar"
- semantic_search_tooltips:
- results_explanation: "Als dit is ingeschakeld, worden hieronder extra AI-zoekresultaten toegevoegd."
- invalid_sort: "Zoekresultaten moeten worden gesorteerd op Relevantie om AI-resultaten weer te geven"
- semantic_search_unavailable_tooltip: "Zoekresultaten moeten worden gesorteerd op Relevantie om AI-resultaten weer te geven"
- ai_generated_result: "Zoekresultaat gevonden met AI"
- quick_search:
- suffix: "in alle topics en berichten met AI"
- ai_artifact:
- expand_view_label: "Weergave uitvouwen"
- collapse_view_label: "Volledig scherm verlaten (ESC of knop Terug)"
- click_to_run_label: "Artefact uitvoeren"
- ai_bot:
- llm: "Model"
- pm_warning: "AI-chatbotberichten worden regelmatig gecontroleerd door moderators."
- cancel_streaming: "Stoppen met antwoorden"
- default_pm_prefix: "[Ongetitelde PB van AI-bot]"
- shortcut_title: "Start een PB met een AI-bot"
- share: "AI-conversatie kopiëren"
- conversation_shared: "Conversatie gekopieerd"
- debug_ai: "Ruw AI-verzoek en -antwoord weergeven"
- debug_ai_modal:
- title: "AI-interactie weergeven"
- copy_request: "Verzoek kopiëren"
- copy_response: "Antwoord kopiëren"
- request_tokens: "Verzoektokens:"
- response_tokens: "Antwoordtokens:"
- request: "Verzoek"
- response: "Antwoord"
- next_log: "Volgende"
- previous_log: "Vorige"
- share_full_topic_modal:
- title: "Conversatie openbaar delen"
- share: "Link delen en kopiëren"
- update: "Link bijwerken en kopiëren"
- delete: "Deling verwijderen"
- share_ai_conversation:
- name: "AI-conversatie delen"
- title: "Deel deze AI-conversatie openbaar"
- invite_ai_conversation:
- button: "Uitnodigen"
- ai_label: "AI"
- ai_title: "Conversatie met AI"
- share_modal:
- title: "AI-conversatie kopiëren"
- copy: "Kopiëren"
- context: "Te delen interacties:"
- share_tip: "Je kunt ook de hele conversatie delen"
- bot_names:
- fake: "Neppe testbot"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "Vandaag"
- last_7_days: "Afgelopen 7 dagen"
- last_30_days: "Afgelopen 30 dagen"
- sentiments:
- dashboard:
- title: "Sentiment"
- sentiment_analysis:
- filter_types:
- all: "Alle"
- positive: "Positief"
- neutral: "Neutraal"
- negative: "Negatief"
- group_types:
- category: "Categorie"
- tag: "Taggen"
- table:
- sentiment: "Sentiment"
- total_count: "Totaal"
- summarization:
- chat:
- title: "Berichten samenvatten"
- description: "Selecteer hieronder een optie om het gesprek samen te vatten dat tijdens de gewenste periode is gevoerd."
- summarize: "Samenvatten"
- since:
- one: "Afgelopen uur"
- other: "Afgelopen %{count} uur"
- topic:
- title: "Topicsamenvatting"
- close: "Samenvattingspaneel sluiten"
- topic_list_layout:
- button:
- compact: "Compact"
- expanded: "Uitgebreid"
- expanded_description: "met AI-samenvattingen"
- discobot_discoveries:
- regular_results: "Topics"
- collapse: "Samenvouwen"
- tooltip:
- actions:
- disable: "Uitschakelen"
- review:
- types:
- reviewable_ai_post:
- title: "Door AI gemarkeerd bericht"
- reviewable_ai_chat_message:
- title: "Door AI gemarkeerd chatbericht"
diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml
deleted file mode 100644
index 59c64be4..00000000
--- a/config/locales/client.pl_PL.yml
+++ /dev/null
@@ -1,617 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-pl_PL:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Umożliwia wyszukiwanie AI"
- stream_completion: "Umożliwia strumieniowe uzupełnianie osobowości AI"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "Emocje"
- reports:
- filters:
- group_by:
- label: "Grupuj według"
- sort_by:
- label: "Sortuj po"
- tag:
- label: "Tag"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Nadawca"
- description: "Użytkownik, który wyśle raport"
- receivers:
- label: "Odbiorcy"
- description: "Użytkownicy, którzy otrzymają raport (e-maile zostaną wysłane bezpośrednio, nazwy użytkowników zostaną wysłane w wiadomości prywatnej)"
- topic_id:
- label: "ID tematu"
- title:
- label: "Tytuł"
- description: "Tytuł raportu"
- days:
- label: "Dni"
- description: "Zakres czasowy raportu"
- offset:
- label: "Przesunięcie"
- description: "Podczas testowania możesz chcieć uruchomić raport historycznie, użyj przesunięcia, aby rozpocząć raport we wcześniejszej dacie"
- instructions:
- label: "Instrukcje"
- description: "Instrukcje dostarczone do dużego modelu językowego"
- sample_size:
- label: "Wielkość próbki"
- description: "Liczba postów do pobrania na potrzeby raportu"
- tokens_per_post:
- label: "Tokeny za post"
- description: "Liczba tokenów LLM do użycia na post"
- model:
- label: "Model"
- description: "LLM do wykorzystania do generowania raportów"
- categories:
- label: "Kategorie"
- description: "Filtruj tematy tylko do tych kategorii"
- tags:
- label: "Tagi"
- description: "Filtruj tematy tylko do tych tagów"
- exclude_tags:
- label: "Wyklucz tagi"
- description: "Wyklucz tematy z tymi tagami"
- exclude_categories:
- label: "Wyklucz kategorie"
- description: "Wyklucz tematy z tych kategorii"
- allow_secure_categories:
- label: "Zezwalaj na bezpieczne kategorie"
- description: "Zezwalaj na generowanie raportu dla tematów w bezpiecznych kategoriach"
- suppress_notifications:
- label: "Wyłącz powiadomienia"
- description: "Pomiń powiadomienia, które raport może generować, przekształcając je w treść. Spowoduje to ponowne przypisanie wzmianek i linków wewnętrznych."
- debug_mode:
- label: "Tryb debugowania"
- description: "Włącz tryb debugowania, aby zobaczyć nieprzetworzone dane wejściowe i wyjściowe LLM."
- priority_group:
- label: "Grupa priorytetowa"
- description: "Nadaj priorytet treściom z tej grupy w raporcie"
- temperature:
- label: "Temperatura"
- description: "Temperatura używana dla LLM. Zwiększ, aby zwiększyć losowość (pozostaw puste, aby użyć domyślnego modelu)."
- llm_tool_triage:
- fields:
- model:
- label: "Model"
- description: "Domyślny model językowy używany do selekcji"
- tool:
- label: "Narzędzie"
- llm_persona_triage:
- fields:
- persona:
- label: "Osobowość"
- silent_mode:
- label: "Tryb cichy"
- description: "W trybie cichym persona otrzyma zawartość, ale nie opublikuje niczego na forum - przydatne podczas przeprowadzania selekcji przy użyciu narzędzi."
- llm_triage:
- fields:
- system_prompt:
- label: "Monit systemowy"
- description: "Wskazówka, która zostanie użyta do trializacji, upewnij się, że odpowiedziałaś jednym słowem, którego możesz użyć do uruchomienia akcji"
- max_post_tokens:
- label: "Maksymalna ilość tokenów postu"
- description: "Maksymalna liczba tokenów do skanowania przy użyciu selekcji LLM"
- stop_sequences:
- description: "Poinstruuj model, aby zatrzymał generowanie tokenów po osiągnięciu jednej z tych wartości"
- search_for_text:
- label: "Wyszukaj tekst"
- description: "Jeśli w odpowiedzi LLM pojawi się następujący tekst, wykonaj następujące czynności"
- category:
- label: "Kategoria"
- description: "Kategoria do zastosowania w temacie"
- tags:
- label: "Tagi"
- description: "Tagi do zastosowania w temacie"
- canned_reply:
- label: "Odpowiedz"
- description: "Nieprzetworzony tekst odpowiedzi na post w temacie"
- canned_reply_user:
- label: "Odpowiedz użytkownikowi"
- description: "Nazwa użytkownika, który ma opublikować odpowiedź w szablonie"
- hide_topic:
- label: "Ukryj temat"
- description: "Spraw, by temat nie był widoczny publicznie, jeśli zostanie uruchomiony."
- flag_type:
- label: "Typ flagi"
- description: "Rodzaj flagi, która ma zostać zastosowana do postu (spam lub wysłanie do sprawdzenia)."
- flag_post:
- label: "Oflaguj post"
- description: "Oflaguj post (jako spam lub do sprawdzenia)"
- include_personal_messages:
- label: "Dołącz osobiste wiadomości"
- description: "Skanuj i sortuj również wiadomości prywatne"
- reply_persona:
- label: "Persona odpowiedzi"
- description: "AI Persona użyta do odpowiedzi (musi mieć domyślną wartość LLM), będzie traktowana priorytetowo nad gotową odpowiedzią"
- model:
- label: "Model"
- description: "Model językowy używany do selekcji"
- temperature:
- label: "Temperatura"
- description: "Temperatura używana dla LLM. Zwiększ, aby zwiększyć losowość (pozostaw puste, aby użyć domyślnego modelu)."
- discourse_ai:
- title: "AI"
- features:
- back: "Poprzednia"
- disabled: "(wyłączone)"
- persona:
- one: "Osobowość:"
- few: "Persony:"
- many: "Persony:"
- other: "Persony:"
- groups: "Grupy:"
- no_persona: "Nie ustawiony"
- no_groups: "Brak"
- edit: "Edytuj"
- expand_list:
- one: "(%{count} więcej)"
- few: "(%{count} więcej)"
- many: "(%{count} więcej)"
- other: "(%{count} więcej)"
- collapse_list: "(pokaż mniej)"
- filters:
- all: "Wszystkie"
- reset: "Przywróć"
- search:
- name: "Szukaj"
- ai_helper:
- name: "Pomocnik"
- proofread: Popraw tekst
- explain: "Wyjaśnij"
- markdown_tables: "Wygeneruj tabelę Markdown"
- custom_prompt: "Niestandardowy prompt"
- spam:
- name: "Spam"
- description: "Identyfikuje potencjalny spam przy użyciu wybranego LLM i oznacza go dla moderatorów witryny w celu sprawdzenia w kolejce przeglądania."
- modals:
- select_option: "Wybierz opcję..."
- spam:
- short_title: "Spam"
- title: "Skonfiguruj obsługę spamu"
- select_llm: "Wybierz LLM"
- custom_instructions: "Instrukcje niestandardowe"
- custom_instructions_help: "Niestandardowe instrukcje specyficzne dla Twojej witryny, aby pomóc sztucznej inteligencji w identyfikacji spamu, np. \"Bądź bardziej agresywny w skanowaniu postów nie w języku angielskim\"."
- last_seven_days: "Ostatnie 7 dni"
- scanned_count: "Przeskanowane posty"
- false_positives: "Nieprawidłowo oznaczony"
- false_negatives: "Pominięty spam"
- spam_detected: "Wykryto spam"
- custom_instructions_placeholder: "Instrukcje dla AI specyficzne dla witryny, aby pomóc w dokładniejszej identyfikacji spamu"
- enable: "Włącz"
- spam_tip: "Wykrywanie spamu przez sztuczną inteligencję będzie skanować pierwsze 3 posty wszystkich nowych użytkowników w tematach publicznych. Oznaczy je do sprawdzenia i zablokuje użytkowników, jeśli istnieje prawdopodobieństwo, że są spamem."
- settings_saved: "Ustawienia zapisane"
- spam_description: "Identyfikuje potencjalny spam przy użyciu wybranego LLM i oznacza go dla moderatorów witryny w celu sprawdzenia w kolejce przeglądania."
- no_llms: "Brak dostępnych LLM"
- test_button: "Test..."
- save_button: "Zapisz zmiany"
- test_modal:
- title: "Test wykrywania spamu"
- post_url_label: "Adres URL lub identyfikator posta"
- result: "Wynik"
- scan_log: "Dziennik skanowania"
- run: "Przeprowadź test"
- spam: "Spam"
- not_spam: "Nie spam"
- errors:
- scan_not_admin:
- action: "Napraw"
- resolved: "Błąd został rozwiązany!"
- usage:
- short_title: "Użycie"
- summary: "Podsumowanie"
- total_tokens: "Wszystkie tokeny"
- tokens_over_time: "Tokeny w czasie"
- features_breakdown: "Wykorzystanie według funkcji"
- feature: "Funkcja"
- usage_count: "Liczba użyć"
- model: "Model"
- models_breakdown: "Wykorzystanie na model"
- users_breakdown: "Wykorzystanie na użytkownika"
- all_features: "Wszystkie funkcje"
- all_models: "Wszystkie modele"
- username: "Nazwa użytkownika"
- total_requests: "Razem zapytań"
- request_tokens: "Tokeny żądania"
- response_tokens: "Tokeny odpowiedzi"
- net_request_tokens: "Tokeny żądania sieci"
- cached_tokens: "Tokeny buforowane"
- cached_request_tokens: "Buforowane tokeny żądań"
- no_users: "Nie znaleziono danych użycia użytkownika"
- no_models: "Nie znaleziono danych o użyciu modelu"
- no_features: "Nie znaleziono danych o użyciu funkcji"
- subheader_description: "Tokeny są podstawowymi jednostkami, których LLM używa do rozumienia i generowania tekstu, dane dotyczące użytkowania mogą wpływać na koszty"
- stat_tooltips:
- total_requests: "Wszystkie prośby kierowane do LLM za pośrednictwem Discourse"
- total_tokens: "Wszystkie tokeny używane podczas monitowania LLM"
- request_tokens: "Tokeny używane, gdy LLM próbuje zrozumieć, co mówisz"
- response_tokens: "Tokeny używane, gdy LLM odpowiada na twój prompt"
- cached_tokens: "Wcześniej przetworzone tokeny żądań, które LLM ponownie wykorzystuje w celu optymalizacji wydajności i kosztów."
- periods:
- last_day: "Ostatnie 24 godziny"
- last_week: "Ostatni tydzień"
- last_month: "Ostatni miesiąc"
- custom: "Niestandardowy..."
- ai_persona:
- ai_tools: "Narzędzia"
- tool_strategies:
- all: "Zastosuj do wszystkich odpowiedzi"
- back: "Poprzednia"
- name: "Nazwa"
- edit: "Edytuj"
- export: "Eksport"
- description: "Opis"
- no_llm_selected: "Nie wybrano modelu językowego"
- use_parent_llm: "Użyj modelu językowego person"
- max_context_posts_help: "Maksymalna liczba postów do wykorzystania jako kontekst dla sztucznej inteligencji podczas odpowiadania użytkownikowi. (domyślnie puste)"
- vision_enabled: Wizja włączona
- vision_enabled_help: Jeśli jest włączona, sztuczna inteligencja będzie próbowała zrozumieć obrazy publikowane przez użytkowników w temacie, w zależności od używanego modelu wspierającego widzenie. Obsługiwane przez najnowsze modele Anthropic, Google i OpenAI.
- vision_max_pixels: Obsługiwany rozmiar obrazu
- vision_max_pixel_sizes:
- low: Niska jakość - najtańsza (256x256)
- medium: Średnia jakość (512x512)
- high: Wysoka jakość - najwolniejsza (1024x1024)
- tool_details: Pokaż szczegóły narzędzia
- tool_details_help: Pokaże użytkownikom końcowym szczegółowe informacje na temat narzędzi uruchomionych przez model językowy.
- mentionable: Zezwalaj na wzmianki
- user: Użytkownik
- create_user: Utwórz użytkownika
- create_user_help: Opcjonalnie możesz przypisać użytkownika do tej osoby. Jeśli to zrobisz, sztuczna inteligencja użyje tego użytkownika do odpowiedzi na żądania.
- default_llm: Domyślny model językowy
- default_llm_help: Domyślny model językowy używany dla tej persony. Wymagane, jeśli chcesz wspomnieć o osobie w postach publicznych.
- system_prompt: Prompt systemowy
- allow_personal_messages: "Zezwalaj na wiadomości osobiste"
- allow_personal_messages_help: "Jeśli ta opcja jest włączona, użytkownicy w dozwolonych grupach mogą wysyłać osobiste wiadomości do tej persony."
- allow_topic_mentions: "Zezwalaj na wzmianki o temacie"
- allow_topic_mentions_help: "Jeśli ta opcja jest włączona, użytkownicy w dozwolonych grupach mogą wspominać o tej personie w tematach."
- force_default_llm: "Zawsze używaj domyślnego modelu języka"
- save: "Zapisz"
- saved: "Persona zapisana"
- enabled: "Włączona?"
- tools: "Włączone narzędzia"
- forced_tools: "Narzędzia wymuszone"
- allowed_groups: "Dozwolone grupy"
- confirm_delete: "Czy na pewno chcesz usunąć tę personę?"
- new: "Nowa persona"
- no_personas: "Nie utworzyłeś jeszcze żadnych person"
- title: "Persony"
- short_title: "Persony"
- delete: "Usuń"
- temperature: "Temperatura"
- temperature_help: "Temperatura używana w LLM. Zwiększ, aby zwiększyć kreatywność (pozostaw puste, aby użyć domyślnej wartości modelu, zazwyczaj od 0,0 do 2,0)."
- priority: "Priorytet"
- priority_help: "Priorytetowe persony są wyświetlane użytkownikom na górze listy person. Jeśli wiele person ma priorytet, zostaną one posortowane alfabetycznie."
- tool_options: "Opcje narzędzi"
- rag_conversation_chunks: "Przeszukuj fragmenty konwersacji"
- response_format:
- open_modal: "Edytuj"
- modal:
- key_title: "Klucz"
- list:
- enabled: "Bot AI?"
- ai_bot:
- title: "Opcje bota AI"
- filters:
- reset: "Przywróć"
- rag:
- options:
- show_indexing_options: "Pokaż opcje przesyłania"
- hide_indexing_options: "Ukryj opcje przesyłania"
- uploads:
- title: "Pliki"
- button: "Dodaj pliki"
- filter: "Filtruj przesyłane pliki"
- indexed: "Zindeksowano"
- indexing: "Indeksowanie"
- uploaded: "Gotowe do indeksowania"
- uploading: "Przesyłanie..."
- remove: "Usuń przesyłanie"
- tools:
- back: "Poprzednia"
- short_title: "Narzędzia"
- export: "Eksport"
- no_tools: "Nie utworzyłeś jeszcze żadnych narzędzi"
- name: "Nazwa"
- name_help: "Nazwa pojawi się w interfejsie użytkownika Discourse i będzie krótkim identyfikatorem, którego będziesz używać do znajdowania narzędzia w różnych ustawieniach. Powinna być ona unikatowa (jest wymagana)"
- new: "Nowe narzędzie"
- tool_name: "Nazwa narzędzia"
- tool_name_help: "Nazwa narzędzia jest prezentowana w dużym modelu językowym. Nie jest ona odrębna, ale jest odrębna dla każdej persony. (persona sprawdza poprawność przy zapisywaniu)"
- description: "Opis"
- description_help: "Jasny opis celu narzędzia dla modelu językowego"
- subheader_description: "Narzędzia rozszerzają możliwości botów AI o zdefiniowane przez użytkownika funkcje JavaScript."
- summary: "Podsumowanie"
- summary_help: "Podsumowanie celu narzędzi wyświetlane użytkownikom końcowym"
- script: "Skrypt"
- parameters: "Parametry"
- save: "Zapisz"
- remove_parameter: "Usuń"
- parameter_required: "Wymagane"
- parameter_enum: "Enum"
- parameter_name: "Nazwa parametru"
- parameter_description: "Opis parametru"
- enum_value: "Wartość enum"
- add_enum_value: "Dodaj wartość enum"
- edit: "Edytuj"
- test: "Przeprowadź test"
- delete: "Usuń"
- saved: "Narzędzie zapisane"
- confirm_delete: "Czy na pewno chcesz usunąć to narzędzie?"
- test_modal:
- title: "Przetestuj narzędzie AI"
- run: "Przeprowadź test"
- result: "Wynik testu"
- llms:
- short_title: "LLMs"
- no_llms: "Nie ma jeszcze LLM"
- new: "Nowy model"
- display_name: "Nazwa"
- name: "Identyfikator modelu"
- provider: "Dostawca"
- tokenizer: "Tokenizer"
- url: "Adres URL usługi hostującej model"
- api_key: "Klucz API usługi hostującej model"
- enabled_chat_bot: "Zezwól na wybór bota AI"
- vision_enabled: "Wizja włączona"
- ai_bot_user: "Użytkownik bota AI"
- save: "Zapisz"
- edit: "Edytuj"
- saved: "Model LLM zapisany"
- back: "Poprzednia"
- confirm_delete: Czy na pewno chcesz usunąć ten model?
- delete: Usuń
- seeded_warning: "Ten model jest wstępnie skonfigurowany w Twojej witrynie i nie można go edytować."
- quotas:
- title: "Limity wykorzystania"
- add_title: "Utwórz nowy limit"
- group: "Grupa"
- max_tokens: "Maksymalna liczba tokenów"
- max_usages: "Maksymalna liczba użyć"
- duration: "Czas trwania"
- confirm_delete: "Czy na pewno chcesz usunąć ten limit?"
- add: "Dodaj limit"
- durations:
- hour: "1 godzina"
- six_hours: "6 godzin"
- day: "24 godziny"
- week: "7 dni"
- custom: "Niestandardowy..."
- hours: "godzin"
- max_tokens_help: "Maksymalna liczba tokenów (słów i znaków), które każdy użytkownik w tej grupie może wykorzystać w określonym czasie. Tokeny to jednostki używane przez modele AI do przetwarzania tekstu - w przybliżeniu 1 token = 4 znaki lub 3/4 słowa."
- max_tokens_required: "Musi być ustawiony, jeśli nie ustawiono maksymalnego użycia"
- max_usages_help: "Maksymalna liczba przypadków, w których każdy użytkownik w tej grupie może użyć modelu AI w określonym czasie. Ten limit jest śledzony dla poszczególnych użytkowników, a nie współdzielony przez grupę."
- max_usages_required: "Musi być ustawiony, jeśli nie ustawiono maksymalnej liczby tokenów"
- usage:
- ai_bot: "Bot AI"
- ai_helper: "Pomocnik"
- ai_persona: "Persona (%{persona})"
- ai_summarization: "Podsumuj"
- ai_embeddings_semantic_search: "Wyszukiwanie AI"
- ai_spam: "Spam"
- model_description:
- none: "Ogólne ustawienia, które działają dla większości modeli językowych"
- anthropic-claude-opus-4-0: "Najbardziej inteligentny model Anthropic"
- anthropic-claude-3-5-haiku-latest: "Szybko i ekonomicznie"
- google-gemini-2-5-flash: "Lekki, szybki i ekonomiczny z multimodalnym rozumowaniem"
- open_ai-o3: "Najbardziej wydajny model rozumowania Open AI"
- open_ai-o4-mini: "Zaawansowany, efektywny kosztowo model rozumowania"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Wydajny, lekki model wielojęzyczny"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Wydajny model wielofunkcyjny"
- mistral-mistral-large-latest: "Najpotężniejszy model Mistral"
- mistral-pixtral-large-latest: "Najpotężniejszy model Mistral zdolny do widzenia"
- preseeded_model_description: "Wstępnie skonfigurowany model open-source wykorzystujący %{model}"
- configured:
- title: "Skonfigurowane LLM"
- preconfigured_llms: "Wybierz swój LLM"
- preconfigured:
- title_no_llms: "Wybierz szablon, aby rozpocząć"
- title: "Nieskonfigurowane szablony LLM"
- description: "LLM (Large Language Models) to narzędzia sztucznej inteligencji zoptymalizowane do zadań takich jak podsumowywanie treści, generowanie raportów, automatyzacja interakcji z klientami oraz ułatwianie moderowania i analizowania forów."
- fake: "Konfiguracja ręczna"
- button: "Skonfiguruj"
- next:
- title: "Następna"
- tests:
- title: "Przeprowadź test"
- running: "Przeprowadzam test..."
- success: "Sukces!"
- failure: "Próba nawiązania kontaktu z modelem zwróciła następujący błąd: %{error}"
- hints:
- display_name: "Nazwa używana do odwoływania się do tego modelu w interfejsie Twojej witryny."
- name: "Uwzględniamy to w wywołaniu API, aby określić, którego modelu będziemy używać"
- enabled_chat_bot: "Jeśli ta opcja jest włączona, użytkownicy mogą wybrać ten model podczas tworzenia wiadomości prywatnych za pomocą bota AI"
- providers:
- google: "Google"
- fake: "Niestandardowe"
- provider_fields:
- organization: "Opcjonalny identyfikator organizacji OpenAI"
- disable_system_prompt: "Wyłącz wiadomość systemową w promptach"
- enable_native_tool: "Włącz obsługę natywnych narzędzi"
- provider_order: "Kolejność dostawców (lista rozdzielona przecinkami)"
- reasoning_tokens: "Liczba tokenów użytych do wnioskowania"
- disable_temperature: "Wyłącz temperaturę (niektóre modele nie obsługują temperatury)"
- related_topics:
- title: "Powiązane tematy"
- pill: "Powiązane"
- ai_helper:
- title: "Zaproponuj zmiany za pomocą AI"
- description: "Wybierz jedną z poniższych opcji, a sztuczna inteligencja zasugeruje Ci nową wersję tekstu."
- selection_hint: "Wskazówka: Możesz także zaznaczyć część tekstu przed otwarciem pomocnika, aby przepisać tylko ten fragment."
- suggest: "Zasugeruj za pomocą AI"
- suggest_errors:
- no_suggestions: "Brak dostępnych sugestii"
- missing_content: "Wprowadź treść, aby wygenerować sugestie."
- context_menu:
- trigger: "Zapytaj AI"
- loading: "AI generuje"
- cancel: "Anuluj"
- confirm: "Potwierdź"
- discard: "Odrzuć"
- changes: "Sugerowane zmiany"
- custom_prompt:
- title: "Niestandardowy prompt"
- placeholder: "Wprowadź niestandardowy monit..."
- submit: "Wyślij prompt"
- translate_prompt: "Przetłumacz na %{language}"
- post_options_menu:
- trigger: "Zapytaj AI"
- title: "Zapytaj AI"
- loading: "AI generuje"
- close: "Zamknij"
- copy: "Kopiuj"
- copied: "Skopiowane!"
- cancel: "Anuluj"
- insert_footnote: "Dodaj przypis"
- footnote_disabled: "Automatyczne wstawianie wyłączone, kliknij przycisk kopiowania i edytuj ręcznie."
- footnote_credits: "Wyjaśnienie przez AI"
- fast_edit:
- suggest_button: "Zaproponuj edycję"
- thumbnail_suggestions:
- title: "Sugerowane miniatury"
- select: "Wybierz"
- selected: "Wybrany"
- image_caption:
- save_caption: "Zapisz"
- automatic_caption_dialog:
- prompt: "Ten post zawiera obrazy bez podpisów. Czy chcesz włączyć automatyczne podpisy przy przesyłaniu zdjęć? (Można to później zmienić w preferencjach)."
- confirm: "Włącz"
- cancel: "Nie pytaj ponownie"
- no_content_error: "Najpierw dodaj zawartość, aby wykonać na niej działania AI."
- reviewables:
- model_used: "Zastosowany model:"
- accuracy: "Dokładność:"
- embeddings:
- back: "Poprzednia"
- save: "Zapisz"
- delete: "Usuń"
- presets: "Wybierz ustawienie wstępne..."
- configure_manually: "Skonfiguruj ręcznie"
- edit: "Edytuj"
- seeded_warning: "To jest wstępnie skonfigurowane na twojej stronie i nie może być edytowane."
- tests:
- title: "Przeprowadź test"
- running: "Przeprowadzam test..."
- success: "Sukces!"
- hints:
- dimensions_warning: "Po zapisaniu wartość ta nie może zostać zmieniona."
- display_name: "Nazwa"
- providers:
- google: "Google"
- fake: "Niestandardowe"
- provider_fields:
- model_name: "Nazwa modelu"
- semantic_search: "Tematy (semantyczne)"
- semantic_search_loading: "Wyszukiwanie większej liczby wyników przy użyciu AI"
- semantic_search_results:
- toggle: "Wyświetlanie %{count} wyników znalezionych przy użyciu AI"
- toggle_hidden: "Ukrywanie %{count} wyników znalezionych przy użyciu AI"
- none: "Przepraszamy, nasze wyszukiwanie AI nie znalazło pasujących tematów"
- unavailable: "Wyniki AI są niedostępne"
- ai_generated_result: "Wynik wyszukiwania znaleziony przy użyciu AI"
- ai_artifact:
- expand_view_label: "Rozszerz widok"
- collapse_view_label: "Wyjdź z trybu pełnoekranowego (przycisk ESC lub Wstecz)"
- ai_bot:
- persona: "Osobowość"
- llm: "Model"
- pm_warning: "Wiadomości wysyłane przez chatboty AI są regularnie monitorowane przez moderatorów."
- cancel_streaming: "Zatrzymaj odpowiedź"
- default_pm_prefix: "[PW bota AI bez tytułu]"
- shortcut_title: "Rozpocznij PW z botem AI"
- share: "Skopiuj rozmowę AI"
- conversation_shared: "Rozmowa skopiowana"
- debug_ai_modal:
- request: "Żądanie"
- response: "Odpowiedź"
- next_log: "Następna"
- previous_log: "Poprzedni"
- share_ai_conversation:
- name: "Udostępnij rozmowę AI"
- invite_ai_conversation:
- button: "Zaproś"
- ai_label: "AI"
- ai_title: "Rozmowa z AI"
- share_modal:
- title: "Skopiuj rozmowę AI"
- copy: "Kopiuj"
- context: "Interakcje do udostępnienia:"
- share_tip: "Alternatywnie możesz udostępnić całą rozmowę"
- bot_names:
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- conversations:
- today: "Dzisiaj"
- last_7_days: "Ostatnie 7 dni"
- last_30_days: "Ostatnie 30 dni"
- sentiments:
- dashboard:
- title: "Sentiment"
- sentiment_analysis:
- share_chart: "Skopiuj link do wykresu"
- filter_types:
- all: "Wszystkie"
- positive: "Pozytywne"
- neutral: "Neutralne"
- negative: "Negatywne"
- group_types:
- category: "Kategoria"
- tag: "Tag"
- table:
- sentiment: "Sentiment"
- total_count: "Łącznie"
- summarization:
- chat:
- title: "Podsumuj wiadomości"
- description: "Wybierz opcję poniżej, aby podsumować rozmowę wysłaną w żądanym przedziale czasowym."
- summarize: "Podsumuj"
- since:
- one: "Ostatnia godzina"
- few: "Ostatnie %{count} godziny"
- many: "Ostatnie %{count} godzin"
- other: "Ostatnie %{count} godzin"
- topic:
- title: "Podsumowanie tematu"
- close: "Zamknij panel podsumowania"
- topic_list_layout:
- button:
- expanded: "Rozszerzony"
- expanded_description: "z podsumowaniami AI"
- discobot_discoveries:
- main_title: "Odkrycia Discobota"
- regular_results: "Tematy"
- tell_me_more: "Powiedz mi więcej..."
- continue_convo: "Kontynuuj rozmowę..."
- loading_convo: "Ładowanie konwersacji"
- collapse: "Zwiń"
- tooltip:
- header: "Wyszukiwanie oparte na AI"
- content: "Wyszukiwanie w języku naturalnym obsługiwane przez %{model}"
- actions:
- info: "Jak to działa?"
- disable: "Wyłącz"
- review:
- types:
- reviewable_ai_post:
- title: "Post oflagowany przez AI"
- reviewable_ai_chat_message:
- title: "Wiadomość na czacie oflagowana przez AI"
diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml
deleted file mode 100644
index 6b67474c..00000000
--- a/config/locales/client.pt.yml
+++ /dev/null
@@ -1,181 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-pt:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Ordenar por"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID do Tópico"
- title:
- label: "Título"
- categories:
- label: "Categorias"
- tags:
- label: "Etiquetas"
- llm_triage:
- fields:
- category:
- label: "Categoria"
- tags:
- label: "Etiquetas"
- canned_reply:
- label: "Responder"
- discourse_ai:
- features:
- back: "Retroceder"
- disabled: "(desativado)"
- groups: "Grupos:"
- no_persona: "Não definido"
- no_groups: "Nenhuma"
- edit: "Editar"
- expand_list:
- one: "(%{count} mais)"
- other: "(%{count} mais)"
- collapse_list: "(mostrar menos)"
- filters:
- all: "Tudo"
- reset: "Repor"
- search:
- name: "Pesquisar"
- spam:
- name: "Spam"
- modals:
- select_option: "Selecione uma opção..."
- spam:
- short_title: "Spam"
- last_seven_days: "Últimos 7 Dias"
- enable: "Ativar"
- test_modal:
- spam: "Spam"
- usage:
- summary: "Resumo"
- username: "Nome de Utilizador"
- total_requests: "Total de pedidos"
- periods:
- last_day: "Últimas 24 horas"
- custom: "Personalizar..."
- ai_persona:
- back: "Retroceder"
- name: "Nome"
- edit: "Editar"
- export: "Exportar"
- description: "Descrição"
- user: Utilizador
- save: "Guardar"
- enabled: "Ativado?"
- allowed_groups: "Grupos permitidos"
- delete: "Eliminar"
- response_format:
- open_modal: "Editar"
- modal:
- key_title: "Chave"
- filters:
- reset: "Repor"
- rag:
- uploads:
- title: "Uploads"
- uploading: "A enviar…"
- tools:
- back: "Retroceder"
- export: "Exportar"
- name: "Nome"
- description: "Descrição"
- summary: "Resumo"
- save: "Guardar"
- remove_parameter: "Remover"
- parameter_required: "Necessário"
- edit: "Editar"
- delete: "Eliminar"
- llms:
- display_name: "Nome"
- save: "Guardar"
- edit: "Editar"
- back: "Retroceder"
- delete: Eliminar
- quotas:
- group: "Grupo"
- max_usages: "Máximo de utilizações"
- duration: "Duração"
- durations:
- hour: "1 hora"
- six_hours: "6 horas"
- day: "24 horas"
- week: "7 dias"
- custom: "Personalizar..."
- hours: "horas"
- usage:
- ai_summarization: "Resumir"
- ai_spam: "Spam"
- next:
- title: "Próximo"
- providers:
- google: "Google"
- fake: "Personalizar"
- ai_helper:
- context_menu:
- cancel: "Cancelar"
- discard: "Descartar"
- post_options_menu:
- close: "Fechar"
- copy: "Copiar"
- copied: "Copiado!"
- cancel: "Cancelar"
- thumbnail_suggestions:
- select: "Selecionar"
- image_caption:
- save_caption: "Guardar"
- automatic_caption_dialog:
- confirm: "Ativar"
- embeddings:
- back: "Retroceder"
- save: "Guardar"
- delete: "Eliminar"
- edit: "Editar"
- display_name: "Nome"
- providers:
- google: "Google"
- fake: "Personalizar"
- ai_bot:
- debug_ai_modal:
- request: "Pedido"
- response: "Resposta"
- next_log: "Próximo"
- previous_log: "Anterior"
- invite_ai_conversation:
- button: "Convidar"
- share_modal:
- copy: "Copiar"
- conversations:
- today: "Hoje"
- last_7_days: "Últimos 7 Dias"
- last_30_days: "Últimos 30 Dias"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Tudo"
- group_types:
- category: "Categoria"
- table:
- total_count: "Total"
- summarization:
- chat:
- summarize: "Resumir"
- discobot_discoveries:
- regular_results: "Tópicos"
- collapse: "Colapsar"
- tooltip:
- actions:
- disable: "Desativar"
diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml
deleted file mode 100644
index 1ca28002..00000000
--- a/config/locales/client.pt_BR.yml
+++ /dev/null
@@ -1,686 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-pt_BR:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Permite pesquisa com IA"
- stream_completion: "Permite transmissão de conclusão com persona de IA"
- site_settings:
- categories:
- discourse_ai: "Discourse IA"
- dashboard:
- emotion:
- title: "Gesto"
- description: "Esta tabela exibe uma contagem de postagens classificadas com um determinado gesto, com o modelo \"SamLowe/roberta-base-go_emotions\""
- reports:
- filters:
- sort_by:
- label: "Ordenar por"
- tag:
- label: "Etiqueta"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Remetente"
- description: "O(a) usuário(a) enviará o relatório"
- receivers:
- label: "Destinatários(as)"
- description: "Os(as) usuários(as) que receberão o relatório (os e-mails serão enviados diretamente por e-mail, e os nomes de usuário(a) por MP)"
- topic_id:
- label: "ID do tópico"
- description: "O ID do tópico no qual postar o relatório"
- title:
- label: "Título"
- description: "O título do relatório"
- days:
- label: "Dias"
- description: "O tempo de duração do relatório"
- offset:
- label: "Deslocamento"
- description: "Durante o teste, você pode querer executar o relatório com base no histórico, use o deslocamento para iniciar o relatório numa data anterior"
- instructions:
- label: "Instruções"
- description: "As instruções fonrecidas ao modelo de linguagem grande"
- sample_size:
- label: "Tamanho da amostra"
- description: "A quantidade de postagens para fazer amostragem para o relatório"
- tokens_per_post:
- label: "Tokens por postagem"
- description: "A quantidade de tokens de LLM para usar por postagem"
- model:
- label: "Modelo"
- description: "O LLM para usar na geração de relatórios"
- categories:
- label: "Categorias"
- description: "Filtrar tópicos apenas para estas categorias"
- tags:
- label: "Etiquetas"
- description: "Filtrar tópicos apenas para estas etiquetas"
- exclude_tags:
- label: "Excluir etiquetas"
- description: "Excluir tópicos com estas etiquetas"
- exclude_categories:
- label: "Excluir categorias"
- description: "Excluir tópicos com estas categorias"
- allow_secure_categories:
- label: "Permitir categorias seguras"
- description: "Permitir que o relatório seja gerado para tópicos em categorias seguras"
- suppress_notifications:
- label: "Suprimir notificações"
- description: "Suprima notificações que podem ser geradas pelo relatório ao transformar em conteúdo. As menções e links internos serão remapeados."
- debug_mode:
- label: "Modo de depuração"
- description: "Ativar o modo de depuração para ver as entradas e saídas não processadas do LLM"
- priority_group:
- label: "Grupo de prioridade"
- description: "Priorizar o conteúdo deste grupo no relatório"
- temperature:
- label: "Temperatura"
- top_p:
- label: "Maior P"
- llm_tool_triage:
- fields:
- model:
- label: "Modelo"
- llm_triage:
- fields:
- system_prompt:
- label: "Prompt do sistema"
- description: "O prompt que será usado para triagem, verifique se responderá com uma única palavra que pode ser usada para acionar a ação"
- max_post_tokens:
- label: "Máximo de Tokens de Postagem"
- description: "A quantidade máxima de tokens para ler usando triagem LLM"
- stop_sequences:
- label: "Parar sequências"
- description: "Ordene ao modelo que interrompa a geração de tokens ao atingir um destes valores"
- search_for_text:
- label: "Pesquisar texto"
- description: "Se o texto a seguir aparecer na resposta do LLM, aplicar estas ações"
- category:
- label: "Categoria"
- description: "Categoria para aplicar no tópico"
- tags:
- label: "Etiquetas"
- description: "Etiquetas para aplicar no tópico"
- canned_reply:
- label: "Responder"
- description: "Texto não processado da resposta pré-preparado para postar no tópico"
- canned_reply_user:
- label: "Usuário(a) de resposta"
- description: "O nome de usuário(a) para postar a resposta pré-preparada"
- hide_topic:
- label: "Ocultar tópico"
- description: "Ocultar visibilidade do tópico para o público se for ativado"
- flag_type:
- label: "Tipo de sinalizador"
- description: "O tipo de sinalizador a ser aplicado na postagem (spam ou sinalização para revisão)"
- flag_post:
- label: "Sinalizar postagem"
- description: "Sinaliza a postagem (como spam ou para revisão)"
- include_personal_messages:
- label: "Incluir mensagens pessoais"
- description: "Também verificar e fazer triagem de mensagens pessoais"
- model:
- label: "Modelo"
- description: "Modelo de linguagem usado para triagem"
- temperature:
- label: "Temperatura"
- discourse_ai:
- title: "IA"
- features:
- back: "Voltar"
- disabled: "(desativada)"
- groups: "Grupos:"
- no_persona: "Não configurado"
- no_groups: "Nenhum"
- edit: "Editar"
- expand_list:
- one: "(mais %{count})"
- other: "(mais %{count})"
- collapse_list: "(exibir menos)"
- filters:
- all: "Tudo"
- reset: "Redefinir"
- search:
- name: "Pesquisar"
- embeddings:
- name: "Incorporações"
- ai_helper:
- name: "Ajudante"
- proofread: Revisar texto
- explain: "Explicar"
- smart_dates: "Datas inteligentes"
- markdown_tables: "Gerar tabela de Markdown"
- custom_prompt: "Prompt personalizado"
- spam:
- name: "Spam"
- description: "Identifica possíveis spams usando o LLM selecionado e os sinaliza para inspeção pela moderação do site na fila de revisão"
- modals:
- select_option: "Selecione uma opção..."
- spam:
- short_title: "Spam"
- title: "Configurar tratamento de spam"
- select_llm: "Selecionar LLM"
- custom_instructions: "Instruções personalizadas"
- custom_instructions_help: "Instruções específicas para seu site para ajudar a orientar a IA na identificação de spam, por exemplo: \"Verificar com mais agressividade postagens que não estão em inglês\"."
- last_seven_days: "Últimos sete dias"
- scanned_count: "Postagens verificadas"
- false_positives: "Sinalização incorreta"
- false_negatives: "Spam perdido"
- spam_detected: "Spam detectado"
- custom_instructions_placeholder: "Instruções específicas para o site para ajudar a IA a identificar spam com mais precisão"
- enable: "Ativar"
- spam_tip: "As três primeiras postagens dos(as) novos(as) usuários(as) nos tópicos públicos serão verificadas pela detecção de spam por IA. Serão sinalizadas para análise e, se forem spam, os(as) usuários(as) serão bloqueados(as)."
- settings_saved: "Configurações salvas"
- spam_description: "Identifica possíveis spams usando o LLM selecionado e os sinaliza para inspeção pela moderação do site na fila de revisão"
- no_llms: "Nenhum LLM disponível"
- test_button: "Testar..."
- save_button: "Salvar alterações"
- test_modal:
- title: "Testar detecção de spam"
- post_url_label: "Postar URL ou ID"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 ou ID da postagem"
- result: "Resultado"
- scan_log: "Registro de verificação"
- run: "Rodar teste"
- spam: "Spam"
- not_spam: "Não é spam"
- stat_tooltips:
- incorrectly_flagged: "Itens que o bot de IA sinalizou como spam, mas a moderação discordou"
- missed_spam: "Itens sinalizados como spam pela comunidade, mas que não foram detectados pelo bot de IA, e a moderação concordou"
- errors:
- scan_not_admin:
- message: "Aviso: a verificação de spam não funcionará corretamente porque a conta de verificação de spam não é administrador(a)"
- action: "Corrigir"
- resolved: "O erro foi solucionado!"
- usage:
- short_title: "Uso"
- summary: "Resumo"
- total_tokens: "Tokens totais"
- tokens_over_time: "Tokens ao longo do tempo"
- features_breakdown: "Uso por recurso"
- feature: "Recurso"
- usage_count: "Contagem de usos"
- model: "Modelo"
- models_breakdown: "Uso por modelo"
- users_breakdown: "Uso por usuário(a)"
- all_features: "Todos os recursos"
- all_models: "Todos os modelos"
- username: "Nome de usuário(a)"
- total_requests: "Pedidos totais"
- request_tokens: "Tokens de pedido"
- response_tokens: "Tokens de resposta"
- net_request_tokens: "Tokens de pedido de rede"
- cached_tokens: "Tokens em cache"
- cached_request_tokens: "Tokens de pedido em cache"
- no_users: "Sem dados de uso do(a) usuário(a) encontrados"
- no_models: "Sem dados de uso de modelo encontrados"
- no_features: "Sem dados de uso de recurso encontrados"
- subheader_description: "Tokens são unidades básicas usadas por LLMs para compreender e gerar texto. Os dados de uso podem afetar o custo"
- stat_tooltips:
- total_requests: "Todos os pedidos feitos para LLMs através do Discourse"
- total_tokens: "Todos os tokens usados ao enviar comandos para um LLM"
- request_tokens: "Tokens usados quando o LLM tentar compreender o que você está dizendo"
- response_tokens: "Tokens usados quando o LLM responder ao seu comando"
- cached_tokens: "Tokens de pedido processados anteriormente reutilizados pelo LLM para otimizar o desempenho e reduzir custo"
- periods:
- last_day: "Últimas 24 horas"
- last_week: "Semana passada"
- last_month: "Mês passado"
- custom: "Personalizado..."
- ai_persona:
- ai_tools: "Ferramentas"
- tool_strategies:
- all: "Aplicar a todas as respostas"
- replies:
- one: "Aplicar apenas à primeira resposta"
- other: "Aplicar às primeiras %{count} respostas"
- back: "Voltar"
- name: "Nome"
- edit: "Editar"
- export: "Exportar"
- description: "Descrição"
- no_llm_selected: "Nenhum modelo de linguagem selecionado"
- max_context_posts: "Máximo de postagens de contexto"
- max_context_posts_help: "A quantidade máxima de postagens para usar como contexto para a IA ao responder ao(à) usuário(a). (deixar vazio para padrão)"
- vision_enabled: Visão ativada
- vision_enabled_help: Ative para a IA tentar entender as imagens postadas pelos(as) usuários(as) no tópico conforme o modelo usado na visão compatível. É compatível com os modelos mais recentes do Anthropic, Google e OpenAI.
- vision_max_pixels: Tamanho de imagem compatível
- vision_max_pixel_sizes:
- low: Baixa qualidade - menor custo (256x256)
- medium: Média qualidade (512x512)
- high: Alta qualidade - mais lento (1024x1024)
- tool_details: Exibir detalhes da ferramenta
- tool_details_help: Serão exibidos aos(às) usuários(as) finais as ferramentas nas quais o modelo de linguagem foi acionado.
- mentionable: Permitir menções
- mentionable_help: Ative para que os(as) usuários(as) nos grupos permitidos possam mencionar este(a) usuário(a) nas postagens. A IA responderá como esta persona.
- user: Usuário(a)
- create_user: Criar usuário(a)
- create_user_help: Como alternativa, você poderá anexar um(a) usuário(a) a esta persona. Se fizer isso, a IA usará este(a) usuário(a) para responder aos pedidos.
- default_llm: Modelo de linguagem padrão
- default_llm_help: O modelo de linguagem padrão a ser usado para esta persona. É obrigatório se você quiser mencionar a persona em postagens públicas.
- question_consolidator_llm: Modelo de linguagem para consolidador de pergunta
- question_consolidator_llm_help: O modelo de linguagem a ser usado para o consolidador de pergunta. Para economizar, você pode escolher um modelo menos robusto.
- system_prompt: Prompt do sistema
- forced_tool_strategy: Estratégia de ferramenta forçada
- allow_chat_direct_messages: "Permitir mensagens diretas do chat"
- allow_chat_direct_messages_help: "Ative para que os(às) usuários(as) nos grupos permitidos possam enviar mensagens diretas a esta persona."
- allow_chat_channel_mentions: "Permitir menções no canal de chat"
- allow_chat_channel_mentions_help: "Ative para os(as) usuários(as) nos grupos permitidos poderem mencionar esta persona nos canais de chat"
- allow_personal_messages: "Permitir mensagens pessoais"
- allow_personal_messages_help: "Ative para que os(as) usuários(as) nos grupos permitidos possam enviar mensagens pessoais a esta persona."
- allow_topic_mentions: "Permtir menções de tópicos"
- allow_topic_mentions_help: "Ative para os(as) usuários(as) nos grupos permitidos poderem mencionar esta persona nos tópicos."
- force_default_llm: "Usar sempre o modelo de linguagem padrão"
- save: "Salvar"
- saved: "Persona salva"
- enabled: "Ativado(a)?"
- tools: "Ferramentas ativadas"
- forced_tools: "Ferramentas forçadas"
- allowed_groups: "Grupos permitidos"
- confirm_delete: "Você tem certeza de que deseja excluir esta persona?"
- new: "Nova persona"
- no_personas: "Você ainda não criou nenhuma persona"
- title: "Personas"
- short_title: "Personas"
- delete: "Excluir"
- temperature: "Temperatura"
- temperature_help: "A Temperatura a ser usada para o LLM. Aumente para incrementar a criatividade (deixe vazio para usar o padrão do modelo, que geralmente é um valor que varia entre 0.0 e 2.0)"
- top_p: "P Máximo"
- top_p_help: "O P Máximo a ser usado para o LLM, aumente para incrementar o fator aleatório (deixe vazio para usar o padrão do modelo, que geralmente é um valor que varia entre 0.0 e 1.0)"
- priority: "Prioridade"
- priority_help: "Personas de prioridade são exibidas aos(às) usuários(as) no topo da lista de personas. Se várias personas tiverem prioridade, serão escolhidas em ordem alfabética."
- tool_options: "Opções de ferramenta"
- rag_conversation_chunks: "Pesquisar pedaços de conversa"
- rag_conversation_chunks_help: "O número de pedaços a serem usados para pesquisas de modelo RAG. Aumente para incrementar a quantidade de contexto que a IA pode usar."
- persona_description: "Personas são um recurso poderoso que permite personalizar o comportamento da engine de IA no seu fórum do Discourse. Atuam como uma \"mensagem de sistema\" que orienta as respostas e as interações da IA, ajudando a criar uma experiência mais personzalidada e envolvente para o(a) usuário(a)."
- response_format:
- open_modal: "Editar"
- modal:
- key_title: "Chave"
- filters:
- reset: "Redefinir"
- rag:
- options:
- rag_chunk_tokens: "Enviar tokens de pedaço"
- rag_chunk_tokens_help: "O número de tokens a ser usado para cada pedaço no modelo RAG. Aumente para incrementar a quantidade de contexto que a IA pode usar. (Altere para indexar novamente todos os envios)"
- rag_chunk_overlap_tokens: "Carregar tokens de sobreposição de pedaço"
- rag_chunk_overlap_tokens_help: "A quantidade de tokens a serem sobrepostos entre as partes no modelo RAG. (Altere para indexar novamente todos os envios)"
- show_indexing_options: "Exibir opções de envio"
- hide_indexing_options: "Ocultar opções de envio"
- uploads:
- title: "Envios"
- button: "Adicionar arquivos"
- filter: "Filtrar envios"
- indexed: "Indexado(a)"
- indexing: "Indexação"
- uploaded: "Pronto(a) para indexação"
- uploading: "Enviando..."
- remove: "Remover envio"
- tools:
- back: "Voltar"
- short_title: "Ferramentas"
- export: "Exportar"
- no_tools: "Você ainda não criou nenhuma ferramenta"
- name: "Nome"
- new: "Nova ferramenta"
- description: "Descrição"
- description_help: "Descrição clara da finalidade da ferramenta para o modelo de linguagem"
- subheader_description: "As ferramentas extendem as funcionalidades dos bots de IA com funções de JavaScript definidas pelo(a) usuário(a)"
- summary: "Resumo"
- summary_help: "Resumo das finalidades das ferramentas a ser exibido para usuários(as) finais"
- script: "Script"
- parameters: "Parâmetros"
- save: "Salvar"
- remove_parameter: "Remover"
- parameter_required: "Necessário(a)"
- parameter_enum: "Enumeração"
- parameter_name: "Nome do parâmetro"
- parameter_description: "Descrição do parâmetro"
- enum_value: "Valor da enumeração"
- add_enum_value: "Adicionar valor da enumeração"
- edit: "Editar"
- test: "Rodar teste"
- delete: "Excluir"
- saved: "Ferramenta salva"
- confirm_delete: "Tem certeza de que deseja excluir esta ferramenta?"
- test_modal:
- title: "Ferramenta de IA de teste"
- run: "Rodar teste"
- result: "Resultado do teste"
- llms:
- short_title: "LLMs"
- no_llms: "Nenhum LLM ainda"
- new: "Novo modelo"
- display_name: "Nome"
- name: "ID do modelo"
- provider: "Provedor"
- tokenizer: "Tokenizador"
- url: "URL do serviço da hospedagem do modelo"
- api_key: "Chave de API do serviço da hospedagem do modelo"
- enabled_chat_bot: "Permitir seletor de bot de IA"
- vision_enabled: "Visão ativada"
- ai_bot_user: "Usuário(a) de bot de IA"
- save: "Salvar"
- edit: "Editar"
- saved: "Modelo de LLM salvo"
- back: "Voltar"
- confirm_delete: Tem certeza de que deseja excluir este modelo?
- delete: Excluir
- seeded_warning: "Este modelo foi pré-configurado no seu site e não pode ser editado."
- quotas:
- title: "Cotas de uso"
- add_title: "Criar nova cota"
- group: "Grupo"
- max_tokens: "Máx. de tokens"
- max_usages: "Máximo de usos"
- duration: "Duração"
- confirm_delete: "Tem certeza de que deseja excluir esta cota?"
- add: "Adicionar cota"
- durations:
- hour: "1 hora"
- six_hours: "6 horas"
- day: "24 horas"
- week: "Sete dias"
- custom: "Personalizado..."
- hours: "horas"
- max_tokens_help: "Quantidade máxima de tokens (palavras e caracteres) que podem ser usados por cada usuário(a) neste grupo durante o período especificado. Tokens são unidades usadas para modelos de IA processarem texto: 1 token equivale a aproximadamente 4 caracteres ou 3/4 de uma palavra."
- max_usages_help: "A quantidade máxima de vezes que cada usuário(a) neste grupo pode usar o modelo de IA durante o período especificado. Essa cota é rastreada por cada usuário(a) individual e não é compartilhada no grupo."
- usage:
- ai_bot: "Bot de IA"
- ai_helper: "Ajudante"
- ai_persona: "Persona (%{persona})"
- ai_summarization: "Resumir"
- ai_embeddings_semantic_search: "Pesquisa com IA"
- ai_spam: "Spam"
- in_use_warning:
- one: "Este modelo é usado atualmente por %{settings}. Se configurado incorretamente, o recurso não funcionará como esperado."
- other: "Este modelo é usado atualmente por %{settings}. Se configurado incorretamente, os recursos não funcionarão como esperado. "
- model_description:
- none: "Configurações gerais que funcionam com a maioria dos modelos de linguagens"
- anthropic-claude-opus-4-0: "Modelo mais inteligente da Anthropic"
- anthropic-claude-3-5-haiku-latest: "Rápido e econômico"
- google-gemini-2-5-flash: "Modelo leve, rápido e econômico com raciocínio multimodal"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Modelo multi-idioma leve e eficiente"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Modelo multifinalidade poderoso"
- mistral-mistral-large-latest: "Modelo mais poderoso da Mistral"
- mistral-pixtral-large-latest: "Modelo mais poderoso com capacidade de visão da Mistral"
- preseeded_model_description: "Modelo de código aberto pré-configurado que utiliza %{model}"
- configured:
- title: "LLMs configuradas"
- preconfigured_llms: "Selecione sua LLM"
- preconfigured:
- title_no_llms: "Selecione um modelo para começar"
- title: "Modelos LLM não configurados"
- description: "LLMs (Large Language Models) são ferramentas de AI otimizadas para tarefas como resumo de conteúdo, geração de relatórios, automatização de interações com cliente, além de ideias e moderação facilitada de fóruns"
- fake: "Configuração manual"
- button: "Configurar"
- next:
- title: "Próximo"
- tests:
- title: "Rodar teste"
- running: "Executando teste..."
- success: "Sucesso!"
- failure: "Erro retornado ao tentar entrar em contato com o modelo: %{error}"
- hints:
- name: "Incluímos na chamada da API para especificar qual modelo será usado"
- vision_enabled: "Ative para a IA tentar entender as imagens. Depende do modelo usado na visão compatível. É compatível com os modelos mais recentes do Anthropic, Google e OpenAI."
- enabled_chat_bot: "Ative para os(as) usuários(as) podesem selecionar este modelo ao criar PM com bot de IA"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Personalizados(as)"
- provider_fields:
- access_key_id: "ID da chave de acesso do AWS Bedrock"
- region: "Região do AWS Bedrock"
- organization: "ID da organização opcional da OpenAI"
- disable_system_prompt: "Desativar mensagens de sistema nos prompts"
- enable_native_tool: "Ativar suporte para ferramenta nativa"
- disable_native_tools: "Desativar suporte para ferramenta nativa (usar ferramentas baseadas em XML)"
- provider_order: "Ordem de provedores (lista separada por vírgula)"
- provider_quantizations: "Ordem de quantizações de provedores (lista separada por vírgula, por exemplo: fp16,fp8)"
- disable_streaming: "Desativar conclusões de transmissão (converter transmissão em pedidos que não forem desse tipo)"
- related_topics:
- title: "Tópicos relacionados"
- pill: "Relacionado(a)"
- ai_helper:
- title: "Sugerir alterações com IA"
- description: "Escolha uma das opções abaixo, e a IA irá sugerir uma nova versão do texto."
- selection_hint: "Dica: antes de abrir o ajudante, você pode selecionar uma parte do texto para reescrever apenas ela."
- suggest: "Sugestão com IA"
- suggest_errors:
- too_many_tags:
- one: "Você pode ter até %{count} etiqueta"
- other: "Você pode ter até %{count} etiquetas"
- no_suggestions: "Sem sugestões disponíveis"
- missing_content: "Insira conteúdo para gerar sugestões."
- context_menu:
- trigger: "Perguntar à IA"
- loading: "A IA está gerando conteúdo"
- cancel: "Cancelar"
- confirm: "Confirmar"
- discard: "Descartar"
- changes: "Edições sugeridas"
- custom_prompt:
- title: "Prompt personalizado"
- placeholder: "Insira um prompt personalizado..."
- submit: "Enviar prompt"
- translate_prompt: "Traduzir para %{language}"
- post_options_menu:
- trigger: "Perguntar à IA"
- title: "Perguntar à IA"
- loading: "A IA está gerando"
- close: "Fechar"
- copy: "Copiar"
- copied: "Copiou!"
- cancel: "Cancelar"
- insert_footnote: "Adicionar nota de rodapé"
- footnote_disabled: "Inserção automática desativada, clique no botão de copiar e edite-a manualmente"
- footnote_credits: "Explicação da IA"
- fast_edit:
- suggest_button: "Sugerir edição"
- thumbnail_suggestions:
- title: "Miniaturas sugeridas"
- select: "Selecionar"
- selected: "Selecionado(a)"
- image_caption:
- button_label: "Legenda com IA"
- generating: "Gerando legenda..."
- credits: "Legendado por IA"
- save_caption: "Salvar"
- automatic_caption_setting: "Ativar legenda automática"
- automatic_caption_loading: "Legendando imagens..."
- automatic_caption_dialog:
- prompt: "Esta postagem contém imagens sem legendas. Gostaria de ativar legendas automáticas em envios de imagem? (Altere nas preferências mais tarde)"
- confirm: "Ativar"
- cancel: "Não perguntar novamente"
- no_content_error: "Primeiro adicione conteúdo para realizar ações de IA nele"
- reviewables:
- model_used: "Modelo usado:"
- accuracy: "Precisão:"
- embeddings:
- short_title: "Incorporações"
- new: "Nova incorporação"
- back: "Voltar"
- save: "Salvar"
- saved: "Configuração de incorporação salva"
- delete: "Excluir"
- confirm_delete: Deseja mesmo remover esta configuração de incorporação?
- empty: "Você ainda não definiu nenhuma incorporação"
- presets: "Selecione uma predefinição..."
- configure_manually: "Configure manualmente"
- edit: "Editar"
- seeded_warning: "Isto foi pré-configurado no seu site e não pode ser editado."
- tests:
- title: "Rodar teste"
- running: "Rodando teste..."
- success: "Sucesso!"
- failure: "Tentando gerar resultado incorporado em %{error}"
- hints:
- dimensions_warning: "Ao ser salvo, este valor não poderá ser alterado."
- matryoshka_dimensions: "Define o tamanho das incorporações aninhadas usadas para representação de dados de forma hierárquica ou multicamada, parecido com o aninhamento de bonecas umas nas outras."
- sequence_length: "A quantidade máxima de tokens que podem ser processados de uma vez ao criar incorporações ou manipular uma consulta."
- distance_function: "Determina como a semelhança entre incorporações é calculada, usando distância do cosseno (medida do ângulo entre vetores) ou o produto interno negativo (medida da sobreposição de valores de vetores)"
- display_name: "Nome"
- provider: "Provedor"
- url: "URL de serviço de incorporações"
- api_key: "Chave de serviço de incorporações"
- tokenizer: "Tokenizador"
- dimensions: "Dimensões de incorporação"
- max_sequence_length: "Tamanho da sequência"
- embed_prompt: "Incorporar comando"
- search_prompt: "Procurar comando"
- matryoshka_dimensions: "Dimensões de Matryoshka"
- distance_function: "Função de distância"
- distance_functions:
- "<#>": "Produto interno negativo"
- <=>: "Distância do cosseno"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Personalizados(as)"
- provider_fields:
- model_name: "Nome do modelo"
- semantic_search: "Tópicos (semântica)"
- semantic_search_loading: "Pesquisando mais resultados usando IA"
- semantic_search_results:
- toggle: "Mostrando %{count} resultados encontrados usando IA"
- toggle_hidden: "Ocultando %{count} resultados encontrados usando IA"
- none: "Desculpe, nossa Pesquisa com IA não achou tópicos correspondentes"
- new: "Aperte \"Pesquisar\"' para começar a procurar resultados novos com a IA"
- unavailable: "Resultados de IA indisponíveis"
- semantic_search_tooltips:
- results_explanation: "Ao ativar esta opção, os resultados de pesquisa com IA são adicionados abaixo."
- invalid_sort: "Os resultados das pesquisas devem ser classificados por ordem de relevância para exibir resultados com IA"
- semantic_search_unavailable_tooltip: "Os resultados das pesquisas devem ser classificados por ordem de relevância para exibir resultados com IA"
- ai_generated_result: "Resultado de pesquisa encontrado usando IA"
- quick_search:
- suffix: "em todos os tópicos e postagens com IA"
- ai_artifact:
- expand_view_label: "Expandir visualização"
- collapse_view_label: "Sair de tela cheia (ESC ou botão de voltar)"
- click_to_run_label: "Rodar artefato"
- ai_bot:
- llm: "Modelo"
- pm_warning: "Todas as mensagens do chatbot de IA são monitoradas regularmente por moderadores(as)."
- cancel_streaming: "Parar resposta"
- default_pm_prefix: "[MP de bot de IA não identificado]"
- shortcut_title: "Iniciar uma MP com bot de IA"
- share: "Copiar conversa com IA"
- conversation_shared: "Conversação copiada"
- debug_ai: "Visualizar resposta e pedido de IA não processado"
- debug_ai_modal:
- title: "Visualizar interação com IA"
- copy_request: "Copiar solicitação"
- copy_response: "Copiar resposta"
- request_tokens: "Tokens de pedido:"
- response_tokens: "Tokens de resposta:"
- request: "Pedir"
- response: "Resposta"
- next_log: "Próximo"
- previous_log: "Anterior"
- share_full_topic_modal:
- title: "Compartilhar conversas publicamente"
- share: "Compartilhar e copiar link"
- update: "Enviar e copiar link"
- delete: "Excluir compartilhamento"
- share_ai_conversation:
- name: "Compartilhar conversação com IA"
- title: "Compartilhar esta conversa com IA publicamente"
- invite_ai_conversation:
- button: "Convite"
- ai_label: "IA"
- ai_title: "Conversação com IA"
- share_modal:
- title: "Copiar conversa com IA"
- copy: "Copiar"
- context: "Interações para compartilhar:"
- share_tip: "Como alternativa, você pode compartilhar toda esta conversa"
- bot_names:
- fake: "Bot de teste simulado"
- claude-3-opus: "Opus Claude 3"
- claude-3-sonnet: "Sonnet Claude 3"
- claude-3-haiku: "Haiku Claude 3"
- cohere-command-r-plus: "Command R Plus Cohere"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "Hoje"
- last_7_days: "Últimos sete dias"
- last_30_days: "Últimos 30 dias"
- sentiments:
- dashboard:
- title: "Sentimento"
- sentiment_analysis:
- filter_types:
- all: "Tudo"
- positive: "Positivo"
- neutral: "Neutro"
- negative: "Negativo"
- group_types:
- category: "Categoria"
- tag: "Etiqueta"
- table:
- sentiment: "Sentimento"
- total_count: "Total"
- summarization:
- chat:
- title: "Resumir mensagens"
- description: "Selecione uma opção abaixo para resumir a conversa enviada durante o período desejado."
- summarize: "Resumir"
- since:
- one: "Última hora"
- other: "Últimas %{count} horas"
- topic:
- title: "Resumo do tópico"
- close: "Fechar painel de resumo"
- topic_list_layout:
- button:
- compact: "Compactar"
- expanded: "Expandido(a)"
- expanded_description: "com resumos de IA"
- discobot_discoveries:
- regular_results: "Tópicos"
- collapse: "Recolher"
- tooltip:
- actions:
- disable: "Desativar"
- review:
- types:
- reviewable_ai_post:
- title: "Postagem sinalizada com IA"
- reviewable_ai_chat_message:
- title: "Mensagem de chat sinalizada com IA"
diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml
deleted file mode 100644
index a5c274d2..00000000
--- a/config/locales/client.ro.yml
+++ /dev/null
@@ -1,184 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ro:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Sortează după"
- tag:
- label: "Etichetă"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID discuție"
- title:
- label: "Titlu"
- categories:
- label: "Categorii"
- tags:
- label: "Etichete"
- llm_triage:
- fields:
- category:
- label: "Categorie"
- tags:
- label: "Etichete"
- canned_reply:
- label: "Răspunde"
- discourse_ai:
- features:
- back: "Înapoi"
- disabled: "(dezactivat)"
- groups: "Grupuri:"
- no_groups: "Nimic"
- edit: "Modifică"
- expand_list:
- one: "(încă %{count})"
- few: "(încă %{count})"
- other: "(încă %{count})"
- collapse_list: "(arată mai puține)"
- filters:
- all: "Toate"
- reset: "Resetare"
- search:
- name: "Căutare"
- spam:
- name: "Spam"
- modals:
- select_option: "Alege o opțiune..."
- spam:
- short_title: "Spam"
- last_seven_days: "Ultimele 7 zile"
- enable: "Activează"
- test_modal:
- spam: "Spam"
- usage:
- summary: "Rezumat"
- username: "Nume utilizator"
- total_requests: "Total cereri"
- periods:
- last_day: "Ultimele 24 de ore"
- custom: "Personalizat..."
- ai_persona:
- back: "Înapoi"
- name: "Nume"
- edit: "Modifică"
- export: "Exportă"
- description: "Descriere"
- user: Utilizatori
- save: "Salvare"
- enabled: "Activat?"
- delete: "Șterge"
- response_format:
- open_modal: "Modifică"
- filters:
- reset: "Resetare"
- rag:
- uploads:
- title: "Urcări"
- uploading: "Se încarcă..."
- tools:
- back: "Înapoi"
- export: "Exportă"
- name: "Nume"
- description: "Descriere"
- summary: "Rezumat"
- save: "Salvează"
- remove_parameter: "Elimină"
- parameter_required: "Necesare"
- edit: "Modifică"
- delete: "Șterge"
- llms:
- display_name: "Nume"
- save: "Salvează"
- edit: "Modifică"
- back: "Înapoi"
- delete: Șterge
- quotas:
- group: "Grup"
- duration: "Durată"
- durations:
- hour: "o oră"
- six_hours: "6 de ore"
- day: "24 de ore"
- custom: "Personalizat..."
- hours: "de ore"
- usage:
- ai_summarization: "Rezumat"
- ai_spam: "Spam"
- next:
- title: "Următorul"
- tests:
- success: "Succes!"
- providers:
- google: "Google"
- fake: "Personalizat"
- ai_helper:
- context_menu:
- cancel: "Anulare"
- discard: "Renunță"
- post_options_menu:
- close: "Închide sondajul"
- copy: "Copiază"
- copied: "Copiat!"
- cancel: "Anulare"
- image_caption:
- save_caption: "Salvare"
- automatic_caption_dialog:
- confirm: "Activează"
- embeddings:
- back: "Înapoi"
- save: "Salvare"
- delete: "Șterge"
- edit: "Modifică"
- tests:
- title: "Rulează test"
- success: "Succes!"
- display_name: "Nume"
- providers:
- google: "Google"
- fake: "Personalizat"
- ai_bot:
- debug_ai_modal:
- request: "Cere"
- response: "Răspuns"
- next_log: "Următorul"
- previous_log: "Precedent"
- invite_ai_conversation:
- button: "Invită"
- share_modal:
- copy: "Copiază"
- conversations:
- today: "Astăzi"
- last_7_days: "Ultimele 7 zile"
- last_30_days: "Ultimele 30 de zile"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Toate"
- group_types:
- category: "Categorie"
- tag: "Etichetă"
- table:
- total_count: "Total"
- summarization:
- chat:
- title: "Rezumați mesajele"
- description: "Selectați o opțiune de mai jos pentru a rezuma conversația trimisă în intervalul de timp dorit."
- summarize: "Rezumat"
- discobot_discoveries:
- regular_results: "Discuții"
- collapse: "Colaps"
- tooltip:
- actions:
- disable: "Dezactivează"
diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml
deleted file mode 100644
index f1d20dbc..00000000
--- a/config/locales/client.ru.yml
+++ /dev/null
@@ -1,696 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ru:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "Разрешает AI-поиск"
- stream_completion: "Включает генерацию ответов AI-персоны в реальном времени"
- site_settings:
- categories:
- discourse_ai: "AI для Discourse"
- dashboard:
- emotion:
- title: "Эмоция"
- description: "В таблице указано количество публикаций, классифицированных по определенной эмоции. Классификация выполнена с помощью модели 'SamLowe/roberta-base-go_emotions'."
- reports:
- filters:
- sort_by:
- label: "Сортировка"
- tag:
- label: "Теги"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Отправитель"
- description: "Пользователь, который отправит отчет"
- receivers:
- label: "Получатели"
- description: "Пользователи, которые получат отчет (электронные письма будут отправлены по электронной почте, имена пользователей будут отправлены в личку)"
- topic_id:
- label: "ID темы"
- description: "Идентификатор темы, в которой будет опубликован отчет"
- title:
- label: "Название"
- description: "Название отчета"
- days:
- label: "Дни"
- description: "Временной интервал отчета"
- offset:
- label: "Смещение"
- description: "При тестировании вы можете запускать отчет с учетом исторических данных, используя смещение начала отчета на более раннюю дату."
- instructions:
- label: "Инструкции"
- description: "Инструкции, предоставленные для большой языковой модели"
- sample_size:
- label: "Размер выборки"
- description: "Количество выбранных публикаций для отчета"
- tokens_per_post:
- label: "Число лексем на публикацию"
- description: "Количество токенов LLM для использования в одной публикации"
- model:
- label: "Модель"
- description: "LLM, используемая для создания отчетов"
- categories:
- label: "Категории"
- description: "Фильтровать темы только по этим категориям"
- tags:
- label: "Теги"
- description: "Фильтровать темы только по этим тегам"
- exclude_tags:
- label: "Исключить теги"
- description: "Исключить темы с этими тегами"
- exclude_categories:
- label: "Исключить категории"
- description: "Исключить темы из этих категорий"
- allow_secure_categories:
- label: "Разрешить защищенные категории"
- description: "Разрешить создание отчета по темам в защищенных категориях"
- suppress_notifications:
- label: "Отключение уведомлений"
- description: "Отключает уведомления, которые может генерировать отчет, путем преобразования их в контент. При этом будут изменены упоминания и внутренние ссылки."
- debug_mode:
- label: "Режим отладки"
- description: "Включить режим отладки для просмотра необработанных входных и выходных данных LLM."
- priority_group:
- label: "Приоритетная группа"
- description: "Приоритизировать контент из этой группы в отчете"
- temperature:
- label: "Температура"
- top_p:
- label: "Top P"
- llm_tool_triage:
- fields:
- model:
- label: "Модель"
- llm_triage:
- fields:
- system_prompt:
- label: "Системный запрос"
- description: "Запрос для приоритизации. Для ответа используйте одно слово, которое можно применить для запуска действия."
- max_post_tokens:
- label: "Макс. число токенов на публикацию"
- description: "Максимальное число токенов, которые может обработать LLM во время триажа"
- stop_sequences:
- label: "Последовательности для остановки"
- description: "Дает указание модели остановить генерацию токенов при получении одного из этих значений"
- search_for_text:
- label: "Поиск текста"
- description: "Если в ответе LLM появится следующий текст, применить эти действия"
- category:
- label: "Категория"
- description: "Категория, применяемая к теме"
- tags:
- label: "Теги"
- description: "Теги, применяемые к теме"
- canned_reply:
- label: "Ответ"
- description: "Необработанный текст шаблонного ответа на публикацию по теме"
- canned_reply_user:
- label: "Ответ пользователя"
- description: "Имя пользователя для публикации шаблонного ответа"
- hide_topic:
- label: "Скрыть тему"
- description: "Сделать тему недоступной для широкой аудитории, если срабатывают определенные условия"
- flag_type:
- label: "Тип метки"
- description: "Тип метки, применяемой к публикации (спам или отправка на проверку)"
- flag_post:
- label: "Пометить публикацию"
- description: "Пометить публикацию (как спам или для отправки на проверку)"
- include_personal_messages:
- label: "Включать личные сообщения"
- description: "Также сканировать и выполнять триаж личных сообщений"
- model:
- label: "Модель"
- description: "Языковая модель для приоритизации"
- temperature:
- label: "Температура"
- discourse_ai:
- title: "AI"
- features:
- back: "Назад"
- disabled: "(отключен)"
- groups: "Группы:"
- no_persona: "Не установлен"
- no_groups: "Нет"
- edit: "Изменить"
- expand_list:
- one: "(ещё %{count})"
- few: "(ещё %{count})"
- many: "(ещё %{count})"
- other: "(ещё %{count})"
- collapse_list: "(свернуть)"
- filters:
- all: "Все настройки"
- reset: "Сбросить"
- search:
- name: "Искать"
- embeddings:
- name: "Встраивания"
- ai_helper:
- name: "Помощник"
- proofread: Вычитать текст
- explain: "Объяснить"
- smart_dates: "Умные даты"
- markdown_tables: "Сгенерировать таблицу в формате Markdown"
- custom_prompt: "Пользовательский запрос"
- spam:
- name: "Спам"
- description: "С помощью выбранной LLM определяет потенциальный спам и отправляет его модераторам сайта в очередь проверки"
- modals:
- select_option: "Выберите вариант..."
- spam:
- short_title: "Спам"
- title: "Настройте обработку спама"
- select_llm: "Выберите LLM"
- custom_instructions: "Пользовательские инструкции"
- custom_instructions_help: "Пользовательские инструкции для вашего сайта помогут AI выявить спам, например, «Более придирчиво проверяй публикации не на английском языке»."
- last_seven_days: "За последние 7 дней"
- scanned_count: "Публикации проверены"
- false_positives: "Неправильно помечено"
- false_negatives: "Пропущенный спам"
- spam_detected: "Обнаружен спам"
- custom_instructions_placeholder: "Инструкции для AI по конкретным сайтам, чтобы более точно выявлять спам"
- enable: "Включить"
- spam_tip: "AI-система обнаружения спама будет проверять первые 3 публикации всех новых пользователей в публичных темах. Она пометит их для проверки и заблокирует пользователей, если они, вероятно, являются спамерами."
- settings_saved: "Настройки сохранены"
- spam_description: "С помощью выбранной LLM определяет потенциальный спам и отправляет его модераторам сайта в очередь проверки"
- no_llms: "Нет доступных LLM"
- test_button: "Тестирование..."
- save_button: "Сохранить изменения"
- test_modal:
- title: "Протестируйте обнаружение спама"
- post_url_label: "URL-адрес или идентификатор публикации"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 или идентификатор публикации"
- result: "Результат"
- scan_log: "Журнал сканирования"
- run: "Запустить тест"
- spam: "Спам"
- not_spam: "Не спам"
- stat_tooltips:
- incorrectly_flagged: "Элементы, которые AI-бот пометил как спам, но модераторы с этим не согласились"
- missed_spam: "Элементы, помеченные сообществом как спам, необнаруженные AI-ботом, но с которыми согласились модераторы"
- errors:
- scan_not_admin:
- message: "Внимание: проверка на спам будет работать некорректно, потому что аккаунт для выявления спама не является аккаунтом администратора"
- action: "Исправить"
- resolved: "Ошибка устранена!"
- usage:
- short_title: "Использование"
- summary: "Сводка"
- total_tokens: "Всего токенов"
- tokens_over_time: "Число токенов с течением времени"
- features_breakdown: "Использование по функциям"
- feature: "Функция"
- usage_count: "Число использований"
- model: "Модель"
- models_breakdown: "Использование по модели"
- users_breakdown: "Использование на пользователя"
- all_features: "Все функции"
- all_models: "Все модели"
- username: "Псевдоним"
- total_requests: "Всего запросов"
- request_tokens: "Токены запроса"
- response_tokens: "Токены ответа"
- net_request_tokens: "Токены сетевых запросов"
- cached_tokens: "Кешированные токены"
- cached_request_tokens: "Кэшированные токены запросов"
- no_users: "Данные об использовании пользователем не найдены"
- no_models: "Данные об использовании модели не найдены"
- no_features: "Данные об использовании функций не найдены"
- subheader_description: "Токены — это основные единицы, которые LLM используют для понимания и генерации текста. Данные об использовании могут влиять на стоимость"
- stat_tooltips:
- total_requests: "Все запросы, направленные LLM через Discourse"
- total_tokens: "Все токены, задействованные при запросе к LLM"
- request_tokens: "Токены, задействованные при попытке LLM понять, что вы говорите"
- response_tokens: "Токены, задействованные при ответе LLM на ваш запрос"
- cached_tokens: "Ранее обработанные токены запросов, которые LLM повторно использует для оптимизации производительности и стоимости"
- periods:
- last_day: "За последние 24 часа"
- last_week: "На прошлой неделе"
- last_month: "В прошлом месяце"
- custom: "Другое…"
- ai_persona:
- ai_tools: "Инструменты"
- tool_strategies:
- all: "Применить ко всем ответам"
- replies:
- one: "Применить только к первому ответу"
- few: "Применить к первым %{count} ответам"
- many: "Применить к первым %{count} ответам"
- other: "Применить к стольким первым ответам: %{count}"
- back: "Назад"
- name: "Название"
- edit: "Изменить"
- export: "Экспорт"
- description: "Описание"
- no_llm_selected: "Языковая модель не выбрана"
- max_context_posts: "Максимум публикаций для учета в контексте"
- max_context_posts_help: "Максимальное количество публикаций, которое будет использоваться в качестве контекста для AI при ответе пользователю. (Пусто по умолчанию)"
- vision_enabled: Визуальное распознавание включено
- vision_enabled_help: Если этот параметр включен, AI будет пытаться анализировать изображения, размещаемые пользователями в теме, при условии, что используемая модель поддерживает визуальное распознавание изображений. Поддерживается новейшими моделями от Anthropic, Google и OpenAI.
- vision_max_pixels: Поддерживаемый размер изображения
- vision_max_pixel_sizes:
- low: Низкое качество — самое дешевое (256×256)
- medium: Среднее качество (512x512)
- high: Высокое качество — самое медленное (1024×1024)
- tool_details: Показать детали инструмента
- tool_details_help: Покажет конечным пользователям сведения о том, какие инструменты запустила языковая модель.
- mentionable: Разрешить упоминания
- mentionable_help: Если этот параметр включен, пользователи в разрешенных группах смогут упоминать этого пользователя в публикациях, AI будет отвечать от имени этой персоны.
- user: Пользователь
- create_user: Создать пользователя
- create_user_help: При желании к этой персоне можно прикрепить пользователя. В этом случае AI будет использовать этого пользователя для ответа на запросы.
- default_llm: Языковая модель по умолчанию
- default_llm_help: Языковая модель по умолчанию, используемая для этой персоны. Требуется, если вы хотите упомянуть персону в общедоступных публикациях.
- question_consolidator_llm: Языковая модель для консолидатора вопросов
- question_consolidator_llm_help: Языковая модель, используемая для консолидатора вопросов; вы можете выбрать менее ресурсозатратную модель для экономии средств.
- system_prompt: Системный запрос
- forced_tool_strategy: Стратегия обязательного применения инструмента
- allow_chat_direct_messages: "Разрешить прямые сообщения в чате"
- allow_chat_direct_messages_help: "Если параметр включен, пользователи в разрешенных группах смогут отправлять прямые сообщения этой персоне."
- allow_chat_channel_mentions: "Разрешить упоминания в каналах чата"
- allow_chat_channel_mentions_help: "Если параметр включен, пользователи в разрешенных группах смогут упоминать эту персону в каналах чата."
- allow_personal_messages: "Разрешить личные сообщения"
- allow_personal_messages_help: "Если параметр включен, пользователи в разрешенных группах смогут отправлять личные сообщения этой персоне."
- allow_topic_mentions: "Разрешить упоминания в темах"
- allow_topic_mentions_help: "Если параметр включен, пользователи в разрешенных группах смогут упоминать эту персону в темах."
- force_default_llm: "Всегда использовать языковую модель по умолчанию"
- save: "Сохранить"
- saved: "Персона сохранена"
- enabled: "Включено?"
- tools: "Включенные инструменты"
- forced_tools: "Обязательные инструменты"
- allowed_groups: "Разрешённые группы"
- confirm_delete: "Точно удалить эту персону?"
- new: "Новая персона"
- no_personas: "Вы еще не создали ни одной персоны"
- title: "Персоны"
- short_title: "Персоны"
- delete: "Удалить"
- temperature: "Температура"
- temperature_help: "Температура — параметр для LLM, его увеличение приводит к повышению креативности (оставьте пустым, чтобы использовать настройки модели по умолчанию, стандартные значения от 0.0 до 2.0)"
- top_p: "Top P"
- top_p_help: "Top P — параметр для LLM, его увеличение приводит к увеличению случайности (оставьте пустым, чтобы использовать настройки модели по умолчанию, стандартные значения от 0.0 до 1.0)"
- priority: "Приоритет"
- priority_help: "Приоритетные персоны показываются пользователям вверху списка персон. Если приоритет имеют несколько персон, они будут отсортированы в алфавитном порядке."
- tool_options: "Параметры инструмента"
- rag_conversation_chunks: "Фрагменты разговора для поиска"
- rag_conversation_chunks_help: "Количество фрагментов для поиска в модели RAG. Увеличьте это значение, чтобы увеличить объем контекста, который может использовать AI."
- persona_description: "Персоны — полезная функция, с помощью которой вы можете настроить поведение движка AI на вашем форуме Discourse. Они действуют как «системное сообщение», задающее направление для ответов AI и обеспечивающее более персонализированное взаимодействие с пользователями."
- response_format:
- open_modal: "Изменить"
- modal:
- key_title: "Ключ"
- filters:
- reset: "Сбросить"
- rag:
- options:
- rag_chunk_tokens: "Токены фрагментов при загрузке"
- rag_chunk_tokens_help: "Количество токенов для каждого фрагмента в модели RAG. Увеличьте, чтобы расширить объем контекста для AI. (Изменение параметра приведет к переиндексации всех загрузок)"
- rag_chunk_overlap_tokens: "Токены перекрытия фрагментов при загрузке"
- rag_chunk_overlap_tokens_help: "Количество токенов для перекрытия между фрагментами в модели RAG. (Изменение параметра приведет к переиндексации всех загрузок)"
- show_indexing_options: "Показать параметры загрузки"
- hide_indexing_options: "Скрыть параметры загрузки"
- uploads:
- title: "Загрузки"
- button: "Добавить файлы"
- filter: "Фильтровать загрузки"
- indexed: "Проиндексированные"
- indexing: "Индексируются"
- uploaded: "Готовые к индексированию"
- uploading: "Загрузка…"
- remove: "Удалить загрузку"
- tools:
- back: "Назад"
- short_title: "Инструменты"
- export: "Экспорт"
- no_tools: "Вы еще не создали ни одного инструмента"
- name: "Название"
- new: "Новый инструмент"
- description: "Описание"
- description_help: "Четкое описание назначения инструмента для языковой модели"
- subheader_description: "Инструменты расширяют возможности AI-ботов с помощью пользовательских функций JavaScript."
- summary: "Сводка"
- summary_help: "Краткое описание назначения инструментов для отображения конечным пользователям"
- script: "Скрипт"
- parameters: "Параметры"
- save: "Сохранить"
- remove_parameter: "Отозвать"
- parameter_required: "Обязательное"
- parameter_enum: "Перечисление"
- parameter_name: "Название параметра"
- parameter_description: "Описание параметра"
- enum_value: "Значение перечисления"
- add_enum_value: "Добавить значение перечисления"
- edit: "Изменить"
- test: "Запустить тест"
- delete: "Удалить"
- saved: "Инструмент сохранен"
- confirm_delete: "Действительно удалить этот инструмент?"
- test_modal:
- title: "Тест AI-инструмента"
- run: "Запустить тест"
- result: "Результат теста"
- llms:
- short_title: "Большие языковые модели"
- no_llms: "Еще нет LLM"
- new: "Новая модель"
- display_name: "Название"
- name: "Идентификатор модели"
- provider: "Поставщик"
- tokenizer: "Токенизатор"
- url: "URL-адрес сервиса, где размещена модель"
- api_key: "API-ключ сервиса, где размещена модель"
- enabled_chat_bot: "Разрешить выбор AI-бота"
- vision_enabled: "Визуальное распознавание включено"
- ai_bot_user: "Пользователь AI-бота"
- save: "Сохранить"
- edit: "Изменить"
- saved: "Модель LLM сохранена"
- back: "Назад"
- confirm_delete: Действительно удалить эту модель?
- delete: Удалить
- seeded_warning: "Эта модель уже предварительно настроена на вашем сайте и ее нельзя изменить."
- quotas:
- title: "Квоты на использование"
- add_title: "Создайте новую квоту"
- group: "Группа"
- max_tokens: "Макс. количество токенов"
- max_usages: "Максимальное количество использований"
- duration: "Период"
- confirm_delete: "Действительно удалить эту квоту?"
- add: "Добавить квоту"
- durations:
- hour: "1 час"
- six_hours: "6 часов"
- day: "24 часа"
- week: "7 дней"
- custom: "Другое…"
- hours: "ч."
- max_tokens_help: "Максимальное количество токенов (слов и символов), которые каждый пользователь в этой группе может использовать в течение указанного периода. Токены — это единицы, используемые AI-моделями для обработки текста. 1 токен равен примерно 4 символам или 3/4 слова."
- max_usages_help: "Максимальное количество раз, которое каждый пользователь в этой группе может использовать AI-модель в течение указанного периода. Эта квота отслеживается для каждого пользователя, а не распределяется по всей группе."
- usage:
- ai_bot: "AI-бот"
- ai_helper: "Помощник"
- ai_persona: "Персона (%{persona})"
- ai_summarization: "Сводка"
- ai_embeddings_semantic_search: "AI-поиск"
- ai_spam: "Спам"
- in_use_warning:
- one: "Эта модель сейчас используется в параметре «%{settings}». В случае неправильной настройки функция не будет работать должным образом."
- few: "Эта модель сейчас используется в следующих параметрах: %{settings}. В случае неправильной настройки функции не будут работать должным образом. "
- many: "Эта модель сейчас используется в следующих параметрах: %{settings}. В случае неправильной настройки функции не будут работать должным образом. "
- other: "Эта модель сейчас используется в следующих параметрах: %{settings}. В случае неправильной настройки функции не будут работать должным образом. "
- model_description:
- none: "Общие настройки, подходящие для большинства языковых моделей"
- anthropic-claude-opus-4-0: "Самая интеллектуальная модель Anthropic"
- anthropic-claude-3-5-haiku-latest: "Быстрая и экономически эффективная модель"
- google-gemini-2-5-flash: "Легкая, быстрая и экономически эффективная модель с поддержкой мультимодальных рассуждений"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Эффективная легкая многоязычная модель"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Мощная многофункциональная модель"
- mistral-mistral-large-latest: "Самая мощная модель Mistral"
- mistral-pixtral-large-latest: "Самая мощная модель Mistral, поддерживающая обработку изображений"
- preseeded_model_description: "Предварительно настроенная модель с открытым исходным кодом, использующая %{model}"
- configured:
- title: "Настроенные LLM"
- preconfigured_llms: "Выберите LLM"
- preconfigured:
- title_no_llms: "Выберите шаблон, чтобы начать"
- title: "Ненастроенные шаблоны LLM"
- description: "LLM (Large Language Models, большие языковые модели) — это инструменты искусственного интеллекта, оптимизированные для таких задач, как обобщение контента, создание отчетов, автоматизация взаимодействия с клиентами, упрощение модерации форумов и анализ информации"
- fake: "Ручная настройка"
- button: "Настроить"
- next:
- title: "Далее"
- tests:
- title: "Запустить тест"
- running: "Запуск теста..."
- success: "Успех!"
- failure: "При попытке связаться с моделью возникла ошибка: %{error}"
- hints:
- name: "Мы включаем эти данные в вызов API, чтобы указать, какую модель будем использовать"
- vision_enabled: "Если этот параметр включен, AI будет пытаться анализировать изображения, при условии, что используемая модель поддерживает визуальное распознавание изображений. Поддерживается новейшими моделями от Anthropic, Google и OpenAI."
- enabled_chat_bot: "Если параметр включен, пользователи могут выбирать эту модель при создании личных сообщений с помощью AI-бота"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Другое"
- provider_fields:
- access_key_id: "Идентификатор ключа доступа к AWS Bedrock"
- region: "Регион AWS Bedrock"
- organization: "Необязательный идентификатор организации OpenAI"
- disable_system_prompt: "Отключить системное сообщение в запросах"
- enable_native_tool: "Включить поддержку встроенных инструментов"
- disable_native_tools: "Отключить поддержку встроенных инструментов (использовать инструменты на основе XML)"
- provider_order: "Порядок поставщиков (список, разделенный запятыми)"
- provider_quantizations: "Порядок квантования поставщиков (список, разделенный запятыми, например: fp16,fp8)"
- disable_streaming: "Отключить генерацию ответов в реальном времени (преобразовывать потоковые запросы в непотоковые)"
- related_topics:
- title: "Связанные темы"
- pill: "Связанные"
- ai_helper:
- title: "Предложить изменения с помощью AI"
- description: "Выберите один из вариантов ниже, и AI предложит новую версию текста."
- selection_hint: "Подсказка: чтобы переписать часть текста, выделите нужный фрагмент до запуска помощника."
- suggest: "Подсказки от AI"
- suggest_errors:
- too_many_tags:
- one: "Максимальное количество тегов — %{count}."
- few: "Максимальное количество тегов — %{count}."
- many: "Максимальное количество тегов — %{count}."
- other: "Максимальное количество тегов — %{count}."
- no_suggestions: "Нет рекомендаций"
- missing_content: "Введите контент для генерации подсказок."
- context_menu:
- trigger: "Спросить AI"
- loading: "AI генерирует ответ"
- cancel: "Отменить"
- confirm: "Подтвердить"
- discard: "Отменить"
- changes: "Предлагаемые правки"
- custom_prompt:
- title: "Пользовательский запрос"
- placeholder: "Введите пользовательский запрос..."
- submit: "Отправить запрос"
- translate_prompt: "Перевести на %{language}"
- post_options_menu:
- trigger: "Спросить AI"
- title: "Спросить AI"
- loading: "AI генерирует ответ"
- close: "Закрыть"
- copy: "Копировать"
- copied: "Скопировано!"
- cancel: "Отменить"
- insert_footnote: "Добавить сноску"
- footnote_disabled: "Автоматическая вставка отключена, нажмите кнопку «Копировать» и отредактируйте вручную."
- footnote_credits: "Объяснение от AI"
- fast_edit:
- suggest_button: "Предложить правку"
- thumbnail_suggestions:
- title: "Рекомендуемые миниатюры"
- select: "Выбрать"
- selected: "Выбрано"
- image_caption:
- button_label: "Подпись к изображению от AI"
- generating: "Создание подписи..."
- credits: "Подпись к изображению от AI"
- save_caption: "Сохранить"
- automatic_caption_setting: "Включить автоматические подписи"
- automatic_caption_loading: "Создание подписей к изображениям..."
- automatic_caption_dialog:
- prompt: "Эта публикация содержит изображения без подписей. Хотите включить автоматические подписи к загружаемым изображениям? (Это можно будет изменить в настройках позже.)"
- confirm: "Включить"
- cancel: "Больше не спрашивать"
- no_content_error: "Сначала добавьте контент, чтобы выполнить действия с ним с помощью AI"
- reviewables:
- model_used: "Использованная модель:"
- accuracy: "Точность:"
- embeddings:
- short_title: "Встраивания"
- new: "Новое встраивание"
- back: "Назад"
- save: "Сохранить"
- saved: "Конфигурация встраивания сохранена"
- delete: "Удалить"
- confirm_delete: Действительно удалить эту конфигурацию встраивания?
- empty: "Вы еще не настроили встраивание"
- presets: "Выберите пресет..."
- configure_manually: "Настроить вручную"
- edit: "Изменить"
- seeded_warning: "Данный параметр уже настроен на вашем сайте и не подлежит изменению."
- tests:
- title: "Выполнить тест"
- running: "Запуск теста..."
- success: "Успех!"
- failure: "Попытка создать встраивание привела к следующему результату: %{error}"
- hints:
- dimensions_warning: "После сохранения это значение изменить нельзя."
- matryoshka_dimensions: "Определяет размер вложенных встраиваний, используемых для иерархического или многослойного представления данных, подобно тому, как вложены друг в друга матрешки."
- sequence_length: "Максимальное количество токенов, которые могут быть обработаны одновременно при создании встраиваний или обработке запроса."
- distance_function: "Определяет, как вычисляется сходство между встраиваниями, используя либо косинусное расстояние (измерение угла между векторами), либо отрицательное скалярное произведение (измерение перекрытия значений векторов)."
- display_name: "Название"
- provider: "Поставщик"
- url: "URL-адрес службы встраивания"
- api_key: "API-ключ службы встраивания"
- tokenizer: "Токенизатор"
- dimensions: "Размеры встраивания"
- max_sequence_length: "Длина последовательности"
- embed_prompt: "Встроенный запрос"
- search_prompt: "Поисковый запрос"
- matryoshka_dimensions: "Размеры матрёшки"
- distance_function: "Функция расстояния"
- distance_functions:
- "<#>": "Отрицательное скалярное произведение"
- <=>: "Косинусное расстояние"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Другое"
- provider_fields:
- model_name: "Название модели"
- semantic_search: "Темы (семантика)"
- semantic_search_loading: "Поиск дополнительных результатов с помощью AI"
- semantic_search_results:
- toggle: "Показать результаты (%{count}), найденные с помощью AI"
- toggle_hidden: "Скрыть результаты (%{count}), найденные с помощью AI"
- none: "К сожалению, AI-поиск не нашел подходящих тем"
- new: "Нажмите «Поиск», чтобы начать поиск новых результатов с помощью AI"
- unavailable: "Результаты AI недоступны"
- semantic_search_tooltips:
- results_explanation: "Если параметр включен, дополнительные результаты AI-поиска будут добавлены ниже."
- invalid_sort: "Результаты поиска должны быть отсортированы по релевантности для отображения AI-результатов"
- semantic_search_unavailable_tooltip: "Результаты поиска должны быть отсортированы по релевантности для отображения AI-результатов"
- ai_generated_result: "Результат поиска найден с помощью AI"
- quick_search:
- suffix: "во всех темах и публикациях с ИИ"
- ai_artifact:
- expand_view_label: "Расширить вид"
- collapse_view_label: "Выйти из полноэкранного режима (кнопка ESC или «Назад»)"
- click_to_run_label: "Запустить артефакт"
- ai_bot:
- llm: "Модель"
- pm_warning: "Сообщения чат-бота с AI регулярно отслеживаются модераторами."
- cancel_streaming: "Прекратить отвечать"
- default_pm_prefix: "[Личные сообщения от AI-бота без названия]"
- shortcut_title: "Начать личный чат с AI-ботом"
- share: "Копировать разговор с AI"
- conversation_shared: "Разговор скопирован"
- debug_ai: "Просмотреть необработанные запросы и ответы AI"
- debug_ai_modal:
- title: "Просмотр взаимодействий с AI"
- copy_request: "Копировать запрос"
- copy_response: "Копировать ответ"
- request_tokens: "Токены запроса:"
- response_tokens: "Токены ответа:"
- request: "Запрос"
- response: "Ответ"
- next_log: "Далее"
- previous_log: "Назад"
- share_full_topic_modal:
- title: "Поделитесь разговором публично"
- share: "Поделиться и скопировать ссылку"
- update: "Обновить и скопировать ссылку"
- delete: "Удалить доступ"
- share_ai_conversation:
- name: "Поделиться разговором с AI"
- title: "Поделитесь этим разговором с AI публично"
- invite_ai_conversation:
- button: "Пригласить"
- ai_label: "AI"
- ai_title: "Разговор с AI"
- share_modal:
- title: "Копировать разговор с AI"
- copy: "Копировать"
- context: "Поделиться взаимодействиями:"
- share_tip: "Вы также можете поделиться всем разговором"
- bot_names:
- fake: "Поддельный тестовый бот"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "Сегодня"
- last_7_days: "За последние 7 дней"
- last_30_days: "За последние 30 дней"
- sentiments:
- dashboard:
- title: "Настроение"
- sentiment_analysis:
- filter_types:
- all: "Все настройки"
- positive: "Позитивное"
- neutral: "Нейтральность"
- negative: "Негативное"
- group_types:
- category: "Категория"
- tag: "Теги"
- table:
- sentiment: "Настроение"
- total_count: "Всего"
- summarization:
- chat:
- title: "Делать сводку сообщений"
- description: "Выберите ниже вариант сводки разговора, отправленного в указанный период."
- summarize: "Сводка"
- since:
- one: "Последний %{count} час"
- few: "Последние %{count} часа"
- many: "Последние %{count} часов"
- other: "Последние %{count} часа"
- topic:
- title: "Сводка по теме"
- close: "Закрыть панель сводки"
- topic_list_layout:
- button:
- compact: "Компактный"
- expanded: "Расширенный"
- expanded_description: "со сводками от AI"
- discobot_discoveries:
- regular_results: "Новые темы"
- collapse: "Свернуть"
- tooltip:
- actions:
- disable: "Отключить"
- review:
- types:
- reviewable_ai_post:
- title: "Публикация с жалобой от AI"
- reviewable_ai_chat_message:
- title: "Сообщение в чате с жалобой от AI"
diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml
deleted file mode 100644
index 77050b41..00000000
--- a/config/locales/client.sk.yml
+++ /dev/null
@@ -1,204 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sk:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Zoradiť podľa"
- tag:
- label: "Značka"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Odosielateľ"
- topic_id:
- label: "ID témy"
- title:
- label: "Názov"
- categories:
- label: "Kategórie"
- tags:
- label: "Štítky"
- llm_triage:
- fields:
- category:
- label: "Kategória"
- tags:
- label: "Štítky"
- canned_reply:
- label: "Odpoveď"
- discourse_ai:
- features:
- back: "Späť"
- disabled: "(vypnuté)"
- groups: "Skupiny:"
- no_persona: "Nie je nastavené"
- no_groups: "Žiadny"
- edit: "Upraviť"
- expand_list:
- one: "(%{count} ďalší)"
- few: "(%{count} ďalšie)"
- many: "(%{count} ďalších)"
- other: "(%{count} ďalší)"
- collapse_list: "(zobraziť menej)"
- filters:
- all: "Všetky"
- reset: "Resetovať"
- search:
- name: "Hľadať"
- spam:
- name: "Spam"
- modals:
- select_option: "Vyberte možnosť..."
- spam:
- short_title: "Spam"
- last_seven_days: "Posledných 7 dní"
- enable: "Povoliť"
- test_modal:
- result: "Výsledok"
- spam: "Spam"
- usage:
- summary: "Zhrnutie"
- username: "Používateľské meno"
- total_requests: "celkový počet žiadostí"
- periods:
- last_day: "Posledných 24 hodín"
- custom: "Vlastné..."
- ai_persona:
- back: "Späť"
- name: "Meno"
- edit: "Upraviť"
- export: "Export"
- description: "Popis"
- user: Používateľ
- save: "Uložiť"
- enabled: "Povolené?"
- delete: "Odstrániť"
- response_format:
- open_modal: "Upraviť"
- modal:
- key_title: "Kľúč"
- filters:
- reset: "Resetovať"
- rag:
- uploads:
- title: "Nahrávanie"
- uploading: "Načítavam..."
- tools:
- back: "Späť"
- export: "Export"
- name: "Meno"
- description: "Popis"
- summary: "Zhrnutie"
- script: "Skript"
- save: "Uložiť"
- remove_parameter: "Odstrániť"
- parameter_required: "Povinné"
- edit: "Upraviť"
- delete: "Odstrániť"
- llms:
- display_name: "Meno"
- save: "Uložiť"
- edit: "Upraviť"
- back: "Späť"
- delete: Odstrániť
- quotas:
- group: "Skupina"
- max_usages: "Maximálne využitie"
- duration: "Trvanie"
- durations:
- hour: "1 hodina"
- six_hours: "6 hodiny"
- day: "24 hodín"
- week: "7 dní"
- custom: "Vlastné..."
- hours: "hodiny"
- usage:
- ai_summarization: "Zhrnúť"
- ai_spam: "Spam"
- next:
- title: "Ďalej"
- tests:
- success: "Úspech!"
- providers:
- google: "Google"
- fake: "Vlastné"
- ai_helper:
- context_menu:
- cancel: "Zrušiť"
- confirm: "Potvrďte"
- discard: "Zahodiť"
- post_options_menu:
- close: "Zavrieť"
- copy: "Kopírovať"
- copied: "Skopírované!"
- cancel: "Zrušiť"
- thumbnail_suggestions:
- select: "Vyberte"
- selected: "Vybrané"
- image_caption:
- save_caption: "Uložiť"
- automatic_caption_dialog:
- confirm: "Povoliť"
- embeddings:
- back: "Späť"
- save: "Uložiť"
- delete: "Odstrániť"
- edit: "Upraviť"
- tests:
- title: "Spustiť test"
- success: "Úspech!"
- display_name: "Meno"
- providers:
- google: "Google"
- fake: "Vlastné"
- ai_bot:
- debug_ai_modal:
- request: "Požiadavka"
- response: "Odpoveď"
- next_log: "Ďalej"
- previous_log: "Predchádzajúci"
- invite_ai_conversation:
- button: "Pozvi"
- share_modal:
- copy: "Kopírovať"
- conversations:
- today: "Dnes"
- last_7_days: "Posledných 7 dní"
- last_30_days: "Posledných 30 dní"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Všetky"
- neutral: "Neutrálna"
- group_types:
- category: "Kategória"
- tag: "Značka"
- table:
- total_count: "Celkovo"
- summarization:
- chat:
- title: "Zhrnutie správ"
- description: "Ak chcete zhrnúť konverzáciu odoslanú počas požadovaného časového obdobia, vyberte nižšie uvedenú možnosť."
- summarize: "Zhrnúť"
- since:
- one: "Posledná hodina"
- few: "Posledných %{count} hodín"
- many: "Posledných %{count} hodín"
- other: "Posledných %{count} hodín"
- discobot_discoveries:
- regular_results: "Témy"
- collapse: "Zbaliť"
- tooltip:
- actions:
- disable: "Zakázať"
diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml
deleted file mode 100644
index 8d0b181e..00000000
--- a/config/locales/client.sl.yml
+++ /dev/null
@@ -1,177 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sl:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Uredi po"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID teme"
- title:
- label: "Naziv"
- categories:
- label: "Kategorije"
- tags:
- label: "Oznake"
- llm_triage:
- fields:
- category:
- label: "Kategorija"
- tags:
- label: "Oznake"
- canned_reply:
- label: "Odgovori"
- discourse_ai:
- features:
- back: "Nazaj"
- disabled: "(onemogočeno)"
- groups: "Skupine:"
- no_persona: "Ni nastavljeno"
- no_groups: "Brez"
- edit: "Uredi"
- expand_list:
- one: "(%{count} več)"
- two: "(%{count} več)"
- few: "(%{count} več)"
- other: "(%{count} več)"
- collapse_list: "(manj)"
- filters:
- all: "Vse"
- reset: "Ponastavi"
- search:
- name: "Išči"
- spam:
- name: "Neželeno"
- modals:
- select_option: "Izberi možnost..."
- spam:
- short_title: "Neželeno"
- last_seven_days: "Zadnjih 7 dni"
- enable: "Omogoči"
- test_modal:
- spam: "Neželeno"
- usage:
- summary: "Povzetek"
- username: "Uporabniško ime"
- periods:
- last_day: "Zadnjih 24 ur"
- custom: "Po meri..."
- ai_persona:
- back: "Nazaj"
- name: "Ime"
- edit: "Uredi"
- export: "Izvozi"
- description: "Opis"
- user: Uporabnik
- save: "Shrani"
- enabled: "Vključeno?"
- allowed_groups: "Dovoljene skupine"
- delete: "Izbriši"
- response_format:
- open_modal: "Uredi"
- filters:
- reset: "Ponastavi"
- rag:
- uploads:
- title: "Prenosi"
- uploading: "Nalagam..."
- tools:
- back: "Nazaj"
- export: "Izvozi"
- name: "Ime"
- description: "Opis"
- summary: "Povzetek"
- save: "Shrani"
- remove_parameter: "Odstrani"
- parameter_required: "Zahtevano"
- edit: "Uredi"
- delete: "Izbriši"
- llms:
- display_name: "Ime"
- save: "Shrani"
- edit: "Uredi"
- back: "Nazaj"
- delete: Izbriši
- quotas:
- group: "Skupina"
- duration: "Trajanje"
- durations:
- hour: "1 ura"
- six_hours: "6 uri"
- day: "24 uri"
- week: "7 dni"
- custom: "Po meri..."
- hours: "ur"
- usage:
- ai_spam: "Neželeno"
- next:
- title: "Naprej"
- tests:
- success: "Uspeh!"
- providers:
- google: "Google"
- fake: "Po meri"
- ai_helper:
- context_menu:
- cancel: "Prekliči"
- confirm: "Potrdi"
- discard: "Zavrzi"
- post_options_menu:
- close: "Zapri"
- copy: "Kopiraj"
- copied: "Kopirano!"
- cancel: "Prekliči"
- image_caption:
- save_caption: "Shrani"
- automatic_caption_dialog:
- confirm: "Omogoči"
- embeddings:
- back: "Nazaj"
- save: "Shrani"
- delete: "Izbriši"
- edit: "Uredi"
- tests:
- success: "Uspeh!"
- display_name: "Ime"
- providers:
- google: "Google"
- fake: "Po meri"
- ai_bot:
- debug_ai_modal:
- request: "Zahteva"
- next_log: "Naprej"
- previous_log: "Prejšnja"
- invite_ai_conversation:
- button: "Povabi"
- share_modal:
- copy: "Kopiraj"
- conversations:
- today: "Danes"
- last_7_days: "Zadnjih 7 dni"
- last_30_days: "Zadnjih 30 dni"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Vse"
- group_types:
- category: "Kategorija"
- table:
- total_count: "Skupaj"
- discobot_discoveries:
- regular_results: "Tem"
- collapse: "Skrči"
- tooltip:
- actions:
- disable: "Onemogoči"
diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml
deleted file mode 100644
index 1bb3d48c..00000000
--- a/config/locales/client.sq.yml
+++ /dev/null
@@ -1,161 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sq:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Rendit sipas"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID e temës"
- title:
- label: "Titulli"
- categories:
- label: "Categories"
- tags:
- label: "Etiketat"
- llm_triage:
- fields:
- category:
- label: "Kategori"
- tags:
- label: "Etiketat"
- canned_reply:
- label: "Përgjigju"
- discourse_ai:
- features:
- back: "Kthehu mbrapa"
- disabled: "(e çaktivizuar)"
- groups: "Grupet:"
- no_persona: "Nuk është vendosur"
- no_groups: "Asnjë"
- edit: "Redaktoni"
- expand_list:
- one: "(%{count} tjetër)"
- other: "(%{count} të tjera)"
- collapse_list: "(shfaq më pak)"
- filters:
- all: "Të Gjithë"
- reset: "Rivendosni"
- search:
- name: "Kërko"
- spam:
- name: "Spam"
- modals:
- select_option: "Zgjidhni një opsion..."
- spam:
- short_title: "Spam"
- enable: "Aktivizo"
- test_modal:
- spam: "Spam"
- usage:
- summary: "Përmbledhja"
- username: "Emri i përdoruesit"
- total_requests: "Total requests"
- ai_persona:
- back: "Kthehu mbrapa"
- name: "Emri"
- edit: "Redakto"
- export: "Eksporto"
- description: "Përshkrimi"
- user: User
- save: "Ruaj"
- enabled: "Aktivizuar?"
- delete: "Fshij"
- response_format:
- open_modal: "Redaktoni"
- filters:
- reset: "Rivendosni"
- rag:
- uploads:
- uploading: "Duke ngarkuar..."
- tools:
- back: "Kthehu mbrapa"
- export: "Eksporto"
- name: "Emri"
- description: "Përshkrimi"
- summary: "Përmbledhja"
- save: "Ruaj"
- remove_parameter: "Hiq"
- parameter_required: "E nevojshme"
- edit: "Redakto"
- delete: "Fshij"
- llms:
- display_name: "Emri"
- save: "Ruaj"
- edit: "Redakto"
- back: "Kthehu mbrapa"
- delete: Fshij
- quotas:
- group: "Grupi"
- max_usages: "Përdorimet maksimale"
- duration: "Kohëzgjatja"
- durations:
- hour: "1 orë"
- six_hours: "6 orë"
- day: "24 orë"
- week: "7 ditë"
- custom: "Me porosi..."
- hours: "orë"
- usage:
- ai_spam: "Spam"
- next:
- title: "Vazhdo përpara"
- providers:
- google: "Google"
- fake: "Grupet e krijuara"
- ai_helper:
- context_menu:
- cancel: "Anulo"
- post_options_menu:
- close: "Mbyll"
- copy: "Kopjo"
- cancel: "Anulo"
- image_caption:
- save_caption: "Ruaj"
- automatic_caption_dialog:
- confirm: "Aktivizo"
- embeddings:
- back: "Kthehu mbrapa"
- save: "Ruani"
- delete: "Fshij"
- edit: "Redaktoni"
- display_name: "Emri"
- providers:
- google: "Google"
- fake: "Grupet e krijuara"
- ai_bot:
- debug_ai_modal:
- next_log: "Vazhdo përpara"
- previous_log: "I kaluar"
- invite_ai_conversation:
- button: "Fto"
- share_modal:
- copy: "Kopjo"
- conversations:
- today: "Sot"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Të Gjithë"
- group_types:
- category: "Kategori"
- table:
- total_count: "Total"
- discobot_discoveries:
- regular_results: "Topics"
- collapse: "Zvogëloni"
- tooltip:
- actions:
- disable: "Çaktivizoni"
diff --git a/config/locales/client.sr.yml b/config/locales/client.sr.yml
deleted file mode 100644
index 7e8441d8..00000000
--- a/config/locales/client.sr.yml
+++ /dev/null
@@ -1,155 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sr:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Sortiraj po"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID Teme"
- title:
- label: "Naslov"
- categories:
- label: "Kategorije"
- llm_triage:
- fields:
- category:
- label: "Kategorija"
- canned_reply:
- label: "Odgovori"
- discourse_ai:
- features:
- back: "Nazad"
- disabled: "(isključeno)"
- groups: "Grupe:"
- no_groups: "Ništa"
- edit: "Izmeni"
- expand_list:
- one: "(još %{count} članova)"
- few: "(još %{count} člana)"
- other: "(još %{count} članova)"
- collapse_list: "(pokaži manje)"
- filters:
- all: "sve"
- reset: "Reset"
- search:
- name: "Pretraži"
- spam:
- name: "Nepoželjno"
- modals:
- select_option: "Izaberi jednu od opcija..."
- spam:
- short_title: "Nepoželjno"
- last_seven_days: "Последњих 7 дана"
- enable: "Omogući"
- test_modal:
- spam: "Nepoželjno"
- usage:
- summary: "Rezime"
- username: "Korisničko Ime"
- periods:
- last_day: "Последња 24 сата"
- ai_persona:
- back: "Nazad"
- name: "Ime foruma"
- edit: "Izmeni"
- export: "Izvoz"
- description: "Opis"
- user: Korisnik
- save: "Sačuvaj"
- enabled: "Omgućeno"
- allowed_groups: "Dozvoljene grupe"
- delete: "Obriši"
- response_format:
- open_modal: "Izmeni"
- filters:
- reset: "Reset"
- rag:
- uploads:
- uploading: "Uploading..."
- tools:
- back: "Nazad"
- export: "Izvoz"
- name: "Ime foruma"
- description: "Opis"
- summary: "Rezime"
- save: "Sačuvaj"
- remove_parameter: "Ukloni"
- parameter_required: "Potrebno"
- edit: "Izmeni"
- delete: "Obriši"
- llms:
- display_name: "Ime foruma"
- save: "Sačuvaj"
- edit: "Izmeni"
- back: "Nazad"
- delete: Obriši
- quotas:
- group: "Grupa"
- usage:
- ai_spam: "Nepoželjno"
- next:
- title: "Dalje"
- providers:
- google: "Google"
- fake: "Posebna"
- ai_helper:
- context_menu:
- cancel: "Odustani"
- discard: "Odbaci"
- post_options_menu:
- close: "Zatvori"
- copy: "Kopija"
- copied: "Kopirano!"
- cancel: "Odustani"
- image_caption:
- save_caption: "Sačuvaj"
- automatic_caption_dialog:
- confirm: "Omogući"
- embeddings:
- back: "Nazad"
- save: "Sačuvaj"
- delete: "Obriši"
- edit: "Izmeni"
- display_name: "Ime foruma"
- providers:
- google: "Google"
- fake: "Posebna"
- ai_bot:
- debug_ai_modal:
- next_log: "Dalje"
- previous_log: "Prethodno"
- invite_ai_conversation:
- button: "Pozovite"
- share_modal:
- copy: "Kopija"
- conversations:
- today: "Danas"
- last_7_days: "Последњих 7 дана"
- last_30_days: "Последњих 30 дана"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "sve"
- group_types:
- category: "Kategorija"
- table:
- total_count: "Ukupno"
- discobot_discoveries:
- regular_results: "Teme"
- collapse: "Spusti"
- tooltip:
- actions:
- disable: "Onemogući"
diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml
deleted file mode 100644
index 4b60621a..00000000
--- a/config/locales/client.sv.yml
+++ /dev/null
@@ -1,198 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sv:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Sortera efter"
- tag:
- label: "Tagg"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Ämnes-ID"
- title:
- label: "Rubrik"
- categories:
- label: "Kategorier"
- tags:
- label: "Taggar"
- llm_triage:
- fields:
- category:
- label: "Kategori"
- tags:
- label: "Taggar"
- canned_reply:
- label: "Svara"
- discourse_ai:
- features:
- back: "Tillbaka"
- disabled: "(inaktiverad)"
- groups: "Grupper:"
- no_persona: "Inte inställd"
- no_groups: "Ingen"
- edit: "Redigera"
- expand_list:
- one: "(ytterligare %{count})"
- other: "(ytterligare %{count})"
- collapse_list: "(visa mindre)"
- filters:
- all: "Alla"
- reset: "Återställ"
- search:
- name: "Sök"
- spam:
- name: "Skräppost"
- modals:
- select_option: "Välj ett alternativ..."
- spam:
- short_title: "Skräppost"
- last_seven_days: "Senaste 7 dagarna"
- enable: "Aktivera"
- test_modal:
- spam: "Skräppost"
- usage:
- summary: "Sammanfattning"
- username: "Användarnamn"
- total_requests: "Totalt antal efterfrågningar"
- periods:
- last_day: "Senaste 24 timmarna"
- custom: "Anpassad..."
- ai_persona:
- back: "Tillbaka"
- name: "Namn"
- edit: "Redigera"
- export: "Exportera"
- description: "Beskrivning"
- user: Användare
- save: "Spara"
- enabled: "Aktiverad?"
- allowed_groups: "Tillåtna grupper"
- delete: "Radera"
- response_format:
- open_modal: "Redigera"
- modal:
- key_title: "Key"
- filters:
- reset: "Återställ"
- rag:
- uploads:
- title: "Uppladdningar"
- uploading: "Laddar upp..."
- tools:
- back: "Tillbaka"
- export: "Exportera"
- name: "Namn"
- description: "Beskrivning"
- summary: "Sammanfattning"
- save: "Spara"
- remove_parameter: "Ta bort"
- parameter_required: "Krävs"
- edit: "Redigera"
- delete: "Ta bort"
- llms:
- display_name: "Namn"
- save: "Spara"
- edit: "Redigera"
- back: "Tillbaka"
- delete: Radera
- quotas:
- group: "Grupp"
- max_usages: "Max antal användningar"
- duration: "Varaktighet"
- durations:
- hour: "1 timme"
- six_hours: "6 timmar"
- day: "24 timmar"
- week: "7 dagar"
- custom: "Anpassad..."
- hours: "timmar"
- usage:
- ai_summarization: "Sammanfatta"
- ai_spam: "Skräppost"
- next:
- title: "Nästa"
- tests:
- success: "Lyckades!"
- providers:
- google: "Google"
- fake: "Anpassad"
- ai_helper:
- context_menu:
- cancel: "Avbryt"
- confirm: "Bekräfta"
- discard: "Förkasta"
- post_options_menu:
- close: "Stäng"
- copy: "Kopiera"
- copied: "Kopierad!"
- cancel: "Avbryt"
- thumbnail_suggestions:
- select: "Välj"
- selected: "Markerat"
- image_caption:
- save_caption: "Spara"
- automatic_caption_dialog:
- confirm: "Aktivera"
- embeddings:
- back: "Tillbaka"
- save: "Spara"
- delete: "Ta bort"
- edit: "Redigera"
- tests:
- success: "Lyckades!"
- display_name: "Namn"
- providers:
- google: "Google"
- fake: "Anpassad"
- ai_bot:
- debug_ai_modal:
- request: "Förfrågning"
- response: "Svar"
- next_log: "Nästa"
- previous_log: "Föregående"
- invite_ai_conversation:
- button: "Bjud in"
- share_modal:
- copy: "Kopiera"
- conversations:
- today: "Idag"
- last_7_days: "Senaste 7 dagarna"
- last_30_days: "Senaste 30 dagarna"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Alla"
- neutral: "Neutralt"
- group_types:
- category: "Kategori"
- tag: "Tagg"
- table:
- total_count: "Totalt"
- summarization:
- chat:
- title: "Sammanfatta meddelanden"
- description: "Välj ett alternativ nedan för att sammanfatta samtalet som skickades under den önskade tidsramen."
- summarize: "Sammanfatta"
- since:
- one: "Senaste timmen"
- other: "Senaste %{count} timmarna"
- topic:
- title: "Sammanfattning av ämne"
- discobot_discoveries:
- regular_results: "Ämnen"
- collapse: "Förminska"
- tooltip:
- actions:
- disable: "Inaktivera"
diff --git a/config/locales/client.sw.yml b/config/locales/client.sw.yml
deleted file mode 100644
index 52db43f0..00000000
--- a/config/locales/client.sw.yml
+++ /dev/null
@@ -1,154 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sw:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Panga kwa"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "Utambulisho wa Mada"
- title:
- label: "Kichwa cha Habari"
- categories:
- label: "Kategoria"
- tags:
- label: "Lebo"
- llm_triage:
- fields:
- category:
- label: "Kategoria"
- tags:
- label: "Lebo"
- canned_reply:
- label: "Jibu"
- discourse_ai:
- features:
- back: "Iliyopita"
- disabled: "(imezuiwa)"
- groups: "Makundi:"
- no_groups: "Hakuna"
- edit: "Hariri"
- filters:
- all: "Vyote"
- reset: "Anza Upya"
- search:
- name: "Tafuta"
- spam:
- name: "Barua Taka"
- modals:
- select_option: "Chagua chaguo moja..."
- spam:
- short_title: "Barua Taka"
- enable: "Wezesha"
- test_modal:
- spam: "Barua Taka"
- usage:
- summary: "Muhtasari"
- username: "Jina la mtumiaji"
- total_requests: "Jumla ya Maombi"
- ai_persona:
- back: "Iliyopita"
- name: "Jina"
- edit: "Hariri"
- export: "Hamisha"
- description: "Elezo"
- user: Mtumiaji
- save: "Hifadhi"
- enabled: "Imeruhusiwa?"
- delete: "Futa"
- response_format:
- open_modal: "Hariri"
- filters:
- reset: "Anza Upya"
- rag:
- uploads:
- title: "Upakiaji"
- uploading: "Inaongezwa"
- tools:
- back: "Iliyopita"
- export: "Hamisha"
- name: "Jina"
- description: "Elezo"
- summary: "Muhtasari"
- save: "Hifadhi"
- remove_parameter: "Ondoa"
- parameter_required: "Muhimu na Inahitajika"
- edit: "Hariri"
- delete: "Futa"
- llms:
- display_name: "Jina"
- save: "Hifadhi"
- edit: "Hariri"
- back: "Iliyopita"
- delete: Futa
- quotas:
- group: "Kikundi"
- usage:
- ai_spam: "Barua Taka"
- next:
- title: "Ijayo"
- tests:
- success: "Mafanikio!"
- providers:
- google: "Google"
- fake: "Binafsi"
- ai_helper:
- context_menu:
- cancel: "Ghairi"
- post_options_menu:
- close: "Funga"
- copy: "Nakili"
- cancel: "Ghairi"
- image_caption:
- save_caption: "Hifadhi"
- automatic_caption_dialog:
- confirm: "Wezesha"
- embeddings:
- back: "Iliyopita"
- save: "Hifadhi"
- delete: "Futa"
- edit: "Hariri"
- tests:
- success: "Mafanikio!"
- display_name: "Jina"
- providers:
- google: "Google"
- fake: "Binafsi"
- ai_bot:
- debug_ai_modal:
- request: "Ombi"
- response: "Mwitikio"
- next_log: "Ijayo"
- previous_log: "Uliopita"
- invite_ai_conversation:
- button: "Mualiko"
- share_modal:
- copy: "Nakili"
- conversations:
- today: "Leo"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "Vyote"
- group_types:
- category: "Kategoria"
- table:
- total_count: "Jumla"
- discobot_discoveries:
- regular_results: "Mada"
- collapse: "Kunja"
- tooltip:
- actions:
- disable: "Sitisha"
diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml
deleted file mode 100644
index 714601e4..00000000
--- a/config/locales/client.te.yml
+++ /dev/null
@@ -1,170 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-te:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "క్రమబద్ధీకరించండి"
- tag:
- label: "ట్యాగ్"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "విషయపు ఐడీ"
- title:
- label: "శీర్షిక"
- categories:
- label: "వర్గాలు"
- tags:
- label: "ట్యాగులు"
- llm_triage:
- fields:
- category:
- label: "వర్గం"
- tags:
- label: "ట్యాగులు"
- canned_reply:
- label: "జవాబు"
- discourse_ai:
- features:
- back: "వెనుకకు"
- no_persona: "సెట్ చేయబడలేదు"
- no_groups: "ఏదీ లేదు"
- edit: "సవరించండి"
- expand_list:
- one: "(మరో %{count})"
- other: "(మరో %{count})"
- filters:
- all: "అన్ని"
- reset: "రీసెట్ చేయండి"
- search:
- name: "శోధించండి"
- spam:
- name: "స్పామ్"
- spam:
- short_title: "స్పామ్"
- enable: "ప్రారంభించండి"
- test_modal:
- spam: "స్పామ్"
- usage:
- summary: "సారాంశం"
- username: "సభ్యనామం"
- total_requests: "మొత్తం అభ్యర్థనలు"
- ai_persona:
- back: "వెనుకకు"
- name: "పేరు"
- edit: "సవరించండి"
- export: "ఎగుమతి"
- description: "వివరణ"
- user: సభ్యుడు
- save: "భద్రపరుచు"
- enabled: "ప్రారంభించబడిందా?"
- delete: "తొలగించు"
- response_format:
- open_modal: "సవరించండి"
- modal:
- key_title: "కీ"
- filters:
- reset: "రీసెట్ చేయండి"
- rag:
- uploads:
- title: "అప్లోడ్లు"
- uploading: "ఎగుమతవుతోంది..."
- tools:
- back: "వెనుకకు"
- export: "ఎగుమతి"
- name: "పేరు"
- description: "వివరణ"
- summary: "సారాంశం"
- save: "సేవ్ చేయండి"
- remove_parameter: "తొలగించు"
- parameter_required: "అవసరం"
- edit: "సవరించండి"
- delete: "తొలగించండి"
- llms:
- display_name: "పేరు"
- save: "సేవ్ చేయండి"
- edit: "సవరించండి"
- back: "వెనుకకు"
- delete: తొలగించండి
- quotas:
- group: "సమూహం"
- max_usages: "గరిష్ట ఉపయోగాలు"
- duration: "వ్యవధి"
- durations:
- hour: "1 గంట"
- six_hours: "6 గంటలు"
- day: "24 గంటలు"
- week: "7 రోజులు"
- hours: "గంటలు"
- usage:
- ai_spam: "స్పామ్"
- next:
- title: "తదుపరి"
- providers:
- google: "గూగుల్"
- fake: "అనుకూల"
- ai_helper:
- context_menu:
- cancel: "రద్దుచేయి"
- confirm: "నిర్ధారించండి"
- discard: "విస్మరించండి"
- post_options_menu:
- close: "మూసివేయి"
- copy: "నకలు"
- copied: "కాపీ చేయబడింది!"
- cancel: "రద్దుచేయి"
- thumbnail_suggestions:
- select: "ఎంచుకోండి"
- selected: "ఎంపిక చేయబడినవి"
- image_caption:
- save_caption: "భద్రపరుచు"
- automatic_caption_dialog:
- confirm: "ప్రారంభించండి"
- embeddings:
- back: "వెనుకకు"
- save: "సేవ్ చేయండి"
- delete: "తొలగించండి"
- edit: "సవరించండి"
- display_name: "పేరు"
- providers:
- google: "గూగుల్"
- fake: "అనుకూల"
- ai_bot:
- debug_ai_modal:
- request: "అభ్యర్థన"
- response: "ప్రతిస్పందన"
- next_log: "తదుపరి"
- previous_log: "గత"
- invite_ai_conversation:
- button: "ఆహ్వానించండి"
- share_modal:
- copy: "నకలు"
- conversations:
- today: "ఈరోజు"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "అన్ని"
- neutral: "తటస్థ"
- group_types:
- category: "వర్గం"
- tag: "ట్యాగ్"
- table:
- total_count: "మొత్తం"
- discobot_discoveries:
- regular_results: "విషయాలు"
- collapse: "కుదించండి"
- tooltip:
- actions:
- disable: "నిలిపివేయండి"
diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml
deleted file mode 100644
index a7b490e9..00000000
--- a/config/locales/client.th.yml
+++ /dev/null
@@ -1,156 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-th:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "เรียงโดย"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "หมายเลขกระทู้"
- title:
- label: "ชื่อเรื่อง"
- categories:
- label: "หมวดหมู่"
- tags:
- label: "ป้าย"
- llm_triage:
- fields:
- category:
- label: "หมวดหมู่"
- tags:
- label: "ป้าย"
- canned_reply:
- label: "ตอบ"
- discourse_ai:
- features:
- back: "กลับ"
- disabled: "(ปิดใช้งานแล้ว)"
- groups: "กลุ่ม:"
- no_groups: "ไม่มี"
- edit: "แก้ไข"
- expand_list:
- other: "(อีก %{count})"
- filters:
- all: "ทั้งหมด"
- search:
- name: "ค้นหา"
- spam:
- name: "ขยะ"
- modals:
- select_option: "เลือกตัวเลือก..."
- spam:
- short_title: "ขยะ"
- last_seven_days: "7 วันที่ผ่านมา"
- enable: "เปิดใช้งาน"
- test_modal:
- spam: "ขยะ"
- usage:
- summary: "สรุป"
- username: "ชื่อผู้ใช้"
- periods:
- last_day: "24 ชั่วโมงที่ผ่านมา"
- ai_persona:
- back: "กลับ"
- name: "ชื่อ"
- edit: "แก้ไข"
- export: "ส่งออก"
- description: "รายละเอียด"
- user: ผู้ใช้
- save: "บันทึก"
- enabled: "เปิดใช้"
- delete: "ลบ"
- response_format:
- open_modal: "แก้ไข"
- rag:
- uploads:
- title: "อัปโหลด"
- uploading: "กำลังอัปโหลด..."
- tools:
- back: "กลับ"
- export: "ส่งออก"
- name: "ชื่อ"
- description: "รายละเอียด"
- summary: "สรุป"
- save: "บันทึก"
- remove_parameter: "ลบ"
- parameter_required: "ต้องการ"
- edit: "แก้ไข"
- delete: "ลบ"
- llms:
- display_name: "ชื่อ"
- save: "บันทึก"
- edit: "แก้ไข"
- back: "กลับ"
- delete: ลบ
- quotas:
- group: "กลุ่ม"
- duration: "ช่วงเวลา"
- usage:
- ai_spam: "ขยะ"
- next:
- title: "ต่อไป"
- tests:
- success: "สำเร็จ!"
- providers:
- google: "กูเกิล"
- ai_helper:
- context_menu:
- cancel: "ยกเลิก"
- post_options_menu:
- close: "ปิด"
- copy: "คัดลอก"
- copied: "คัดลอกแล้ว!"
- cancel: "ยกเลิก"
- image_caption:
- save_caption: "บันทึก"
- automatic_caption_dialog:
- confirm: "เปิดใช้งาน"
- embeddings:
- back: "กลับ"
- save: "บันทึก"
- delete: "ลบ"
- edit: "แก้ไข"
- tests:
- success: "สำเร็จ!"
- display_name: "ชื่อ"
- providers:
- google: "กูเกิล"
- ai_bot:
- debug_ai_modal:
- request: "ร้องขอ"
- next_log: "ต่อไป"
- previous_log: "ก่อนหน้า"
- invite_ai_conversation:
- button: "เชิญ"
- share_modal:
- copy: "คัดลอก"
- conversations:
- today: "วันนี้"
- last_7_days: "7 วันที่ผ่านมา"
- last_30_days: "30 วันที่ผ่านมา"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "ทั้งหมด"
- group_types:
- category: "หมวดหมู่"
- table:
- total_count: "ทั้งหมด"
- discobot_discoveries:
- regular_results: "หัวข้อ"
- collapse: "ย่อ"
- tooltip:
- actions:
- disable: "ปิดใช้งาน"
diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml
deleted file mode 100644
index 6cdf1757..00000000
--- a/config/locales/client.tr_TR.yml
+++ /dev/null
@@ -1,687 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-tr_TR:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "YZ aramasına izin verir"
- stream_completion: "YZ kişilik tamamlamalarının yayınlanmasına izin verir"
- site_settings:
- categories:
- discourse_ai: "Discourse YZ"
- dashboard:
- emotion:
- title: "Duygu"
- description: "Tablo, belirli bir duyguyla sınıflandırılan gönderilerin sayısını listeler. \"SamLowe/roberta-base-go_emotions\" modeliyle sınıflandırılmıştır."
- reports:
- filters:
- sort_by:
- label: "Sıralama ölçütü"
- tag:
- label: "Etiket"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Gönderen"
- description: "Raporu gönderecek kullanıcı"
- receivers:
- label: "Alıcılar"
- description: "Raporu alacak kullanıcılar (e-postalara doğrudan e-posta gönderilecek, kullanıcı adlarına kişisel mesaj gönderilecek)"
- topic_id:
- label: "Konu kimliği"
- description: "Raporun gönderileceği konu kimliği"
- title:
- label: "Başlık"
- description: "Raporun başlığı"
- days:
- label: "Gün"
- description: "Raporun zaman aralığı"
- offset:
- label: "Ofset"
- description: "Test yaparken raporu tarihsel olarak çalıştırmak isteyebilirsiniz, raporu daha önceki bir tarihte başlatmak için ofset kullanın"
- instructions:
- label: "Talimatlar"
- description: "Büyük dil modeline sağlanan talimatlar"
- sample_size:
- label: "Örnek Boyutu"
- description: "Rapor için örneklenecek gönderi sayısı"
- tokens_per_post:
- label: "Gönderi başına token"
- description: "Gönderi başına kullanılacak LLM token'ı sayısı"
- model:
- label: "Model"
- description: "Rapor oluşturmak için kullanılacak LLM"
- categories:
- label: "Kategoriler"
- description: "Konuları yalnızca bu kategorilere filtrele"
- tags:
- label: "Etiketler"
- description: "Konuları yalnızca bu etiketlere filtrele"
- exclude_tags:
- label: "Etiketleri Hariç Tut"
- description: "Bu etiketlere sahip konuları hariç tut"
- exclude_categories:
- label: "Kategorileri Dışla"
- description: "Bu kategorilere sahip konuları hariç tut"
- allow_secure_categories:
- label: "Güvenli kategorilere izin ver"
- description: "Güvenli kategorilerdeki konular için rapor oluşturulmasına izin ver"
- suppress_notifications:
- label: "Bildirimleri bastır"
- description: "Raporun içeriğe dönüştürerek oluşturabileceği bildirimleri bastırın. Bu, bahsedenleri ve dâhili bağlantıları yeniden eşleştirir."
- debug_mode:
- label: "Hata Ayıklama Modu"
- description: "LLM'nin ham giriş ve çıkışını görmek için hata ayıklama modunu etkinleştirin"
- priority_group:
- label: "Öncelikli Grup"
- description: "Raporda bu gruptan gelen içeriği önceliklendirin"
- temperature:
- label: "Sıcaklık"
- top_p:
- label: "Üst P"
- llm_tool_triage:
- fields:
- model:
- label: "Model"
- llm_triage:
- fields:
- system_prompt:
- label: "Sistem İstemi"
- description: "Triyaj için kullanılacak istem, eylemi tetiklemek için kullanabileceğiniz tek bir kelime ile yanıt verdiğinden emin olun"
- max_post_tokens:
- label: "Maksimum Gönderi Token'ı Sayısı"
- description: "LLM triyajı kullanılarak taranacak maksimum token sayısı"
- stop_sequences:
- label: "Durdurma Dizileri"
- description: "Modele, bu değerlerden birine ulaşıldığında token oluşturmayı durdurması talimatını verin"
- search_for_text:
- label: "Metin ara"
- description: "LLM yanıtında aşağıdaki metin görünürse bu eylemleri uygulayın"
- category:
- label: "Kategori"
- description: "Konuya uygulanacak kategori"
- tags:
- label: "Etiketler"
- description: "Konuya uygulanacak etiketler"
- canned_reply:
- label: "Yanıtla"
- description: "Konuyla ilgili gönderiye verilen hazır yanıtın ham metni"
- canned_reply_user:
- label: "Kullanıcıyı yanıtla"
- description: "Hazır yanıtı gönderecek kullanıcının kullanıcı adı"
- hide_topic:
- label: "Konuyu gizle"
- description: "Tetiklenirse konuyu herkese görünmez hâle getirin"
- flag_type:
- label: "Bayrak türü"
- description: "Gönderiye uygulanacak bayrak türü (spam veya sadece inceleme için ekleyin)"
- flag_post:
- label: "Gönderiye bayrak ekle"
- description: "Gönderiye bayrak ekler (spam olarak veya inceleme için)"
- include_personal_messages:
- label: "Kişisel mesaj dâhil edin"
- description: "Ayrıca kişisel mesajları tarayın ve önceliklendirin"
- model:
- label: "Model"
- description: "Triyaj için kullanılan dil modeli"
- temperature:
- label: "Sıcaklık"
- discourse_ai:
- title: "YZ"
- features:
- back: "Geri"
- disabled: "(devre Dışı)"
- groups: "Grup:"
- no_persona: "Ayarlanmamış"
- no_groups: "Yok"
- edit: "Düzenle"
- expand_list:
- one: "(%{count} tane Daha)"
- other: "(%{count} tane Daha)"
- collapse_list: "(daha az göster)"
- filters:
- all: "Hepsi"
- reset: "Sıfırla"
- search:
- name: "Ara"
- embeddings:
- name: "Gömmeler"
- ai_helper:
- name: "Yardımcı"
- proofread: Yazım hataları düzeltilmiş metin
- explain: "Açıkla"
- smart_dates: "Akıllı tarihler"
- markdown_tables: "Markdown tablosu oluştur"
- custom_prompt: "Özel istem"
- spam:
- name: "İstenmeyen içerik"
- description: "Seçili LLM'i kullanarak potansiyel istenmeyen içerikleri belirler ve inceleme kuyruğunda site moderatörlerinin incelemesi için işaretler"
- modals:
- select_option: "Seçenek belirleyin..."
- spam:
- short_title: "İstenmeyen içerik"
- title: "İstenmeyen içerik işlemeyi yapılandırın"
- select_llm: "LLM seç"
- custom_instructions: "Özel talimatlar"
- custom_instructions_help: "YZ'nin istenmeyen içerikleri tespit etmesine yardımcı olmak için sitenize özel talimatlar, ör. \"İngilizce olmayan gönderileri tarama konusunda daha agresif ol\"."
- last_seven_days: "Son 7 gün"
- scanned_count: "Taranan gönderiler"
- false_positives: "Yanlış bayrak eklenmiş"
- false_negatives: "Kaçırılan istenmeyen içerik"
- spam_detected: "İstenmeyen içerik algılandı"
- custom_instructions_placeholder: "YZ'nin istenmeyen içerikleri daha doğru bir şekilde belirlemesine yardımcı olmak için siteye özel talimatlar"
- enable: "Etkinleştir"
- spam_tip: "YZ istenmeyen içerik tespiti, herkese açık konulardaki tüm yeni kullanıcıların ilk 3 gönderisini tarar. Bunları inceleme için işaretler ve spam olma ihtimalleri varsa kullanıcıları engeller."
- settings_saved: "Ayarlar kaydedildi"
- spam_description: "Seçili LLM'i kullanarak potansiyel istenmeyen içerikleri belirler ve inceleme kuyruğunda site moderatörlerinin incelemesi için işaretler"
- no_llms: "LLM mevcut değil"
- test_button: "Test..."
- save_button: "Değişiklikleri kaydet"
- test_modal:
- title: "İstenmeyen içerik algılamayı test edin"
- post_url_label: "Gönderi URL'si veya kimliği"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 veya gönderi kimliği"
- result: "Sonuç"
- scan_log: "Tarama günlüğü"
- run: "Testi çalıştır"
- spam: "İstenmeyen içerik"
- not_spam: "İstenmeyen içerik değil"
- stat_tooltips:
- incorrectly_flagged: "YZ botunun spam olarak bayrak eklediği ve moderatörlerin aynı fikirde olmadığı ögeler"
- missed_spam: "YZ botu tarafından tespit edilemeyen ve moderatörlerin de onayladığı, topluluk tarafından istenmeyen içerik olarak bayrak eklenen ögeler"
- errors:
- scan_not_admin:
- message: "Uyarı: İstenmeyen içerik tarama hesabı bir yönetici olmadığından istenmeyen içerik tarama düzgün çalışmaz"
- action: "Düzelt"
- resolved: "Hata çözüldü!"
- usage:
- short_title: "Kullanım"
- summary: "Özet"
- total_tokens: "Toplam token sayısı"
- tokens_over_time: "Zaman içindeki token'lar"
- features_breakdown: "Özellik başına kullanım"
- feature: "Özellik"
- usage_count: "Kullanım sayısı"
- model: "Model"
- models_breakdown: "Model başına kullanım"
- users_breakdown: "Kullanıcı başına kullanım"
- all_features: "Tüm özellikler"
- all_models: "Tüm modeller"
- username: "Kullanıcı Adı"
- total_requests: "Toplam talep sayısı"
- request_tokens: "Talep token'ları"
- response_tokens: "Karşılık token'ları"
- net_request_tokens: "Net talep token'ları"
- cached_tokens: "Önbelleğe alınan token'lar"
- cached_request_tokens: "Önbelleğe alınmış talep token'ları"
- no_users: "Kullanıcı kullanım verisi bulunamadı"
- no_models: "Model kullanım verisi bulunamadı"
- no_features: "Özellik kullanım verisi bulunamadı"
- subheader_description: "Token'lar, LLM'lerin metinleri anlamak ve üretmek için kullandıkları temel birimlerdir, kullanım verileri maliyetleri etkileyebilir"
- stat_tooltips:
- total_requests: "Discourse aracılığıyla LLM'lere yapılan tüm talepler"
- total_tokens: "LLM istenirken kullanılan tüm token'lar"
- request_tokens: "LLM'in söylediklerinizi anlamaya çalışırken kullandığı token'lar"
- response_tokens: "LLM'in isteminize yanıt verdiğinde kullanılan token'lar"
- cached_tokens: "LLM'in performansı ve maliyeti optimize etmek için yeniden kullandığı daha önce işlenmiş talep token'ları"
- periods:
- last_day: "Son 24 saat"
- last_week: "Geçen hafta"
- last_month: "Geçen ay"
- custom: "Özel..."
- ai_persona:
- ai_tools: "Araçlar"
- tool_strategies:
- all: "Tüm yanıtlara uygula"
- replies:
- one: "Yalnızca ilk yanıta uygula"
- other: "İlk %{count} yanıta uygula"
- back: "Geri"
- name: "Ad"
- edit: "Düzenle"
- export: "Dışa Aktar"
- description: "Açıklama"
- no_llm_selected: "Dil modeli seçilmedi"
- max_context_posts: "Maksimum bağlam gönderisi sayısı"
- max_context_posts_help: "Bir kullanıcıya yanıt verirken YZ için bağlam olarak kullanılacak maksimum gönderi sayısı. (varsayılan için boş)"
- vision_enabled: Vizyon etkin
- vision_enabled_help: Etkinleştirilirse YZ, kullanıcıların konuya gönderdiği görüntüleri anlamaya çalışır; bu, vizyonu destekleyen kullanılan modele bağlıdır. Anthropic, Google ve OpenAI'ın en son modelleri tarafından desteklenir.
- vision_max_pixels: Desteklenen görüntü boyutu
- vision_max_pixel_sizes:
- low: Düşük kalite - en ucuz (256x256)
- medium: Orta kalite (512x512)
- high: Yüksek kalite - en yavaş (1024x1024)
- tool_details: Araç ayrıntılarını göster
- tool_details_help: Son kullanıcılara dil modelinin hangi araçları tetiklediğine ilişkin ayrıntıları gösterir.
- mentionable: Bahsetmelere izin ver
- mentionable_help: Etkinleştirilirse izin verilen gruplardaki kullanıcılar gönderilerde bu kullanıcıdan bahsedebilir, YZ bu kişi olarak yanıt verir.
- user: Kullanıcı
- create_user: Kullanıcı oluştur
- create_user_help: İsteğe bağlı olarak bu kişiye bir kullanıcı ekleyebilirsiniz. Bunu yaparsanız YZ isteklere yanıt vermek için bu kullanıcıyı kullanır.
- default_llm: Varsayılan dil modeli
- default_llm_help: Bu kişi için kullanılacak varsayılan dil modeli. Herkese açık gönderilerde kişiden bahsetmek istiyorsanız gereklidir.
- question_consolidator_llm: Soru Konsolidatörü için Dil Modeli
- question_consolidator_llm_help: Soru birleştirici için kullanılacak dil modeli, maliyetten tasarruf etmek için daha az güçlü bir model seçebilirsiniz.
- system_prompt: Sistem istemi
- forced_tool_strategy: Zorlanmış araç stratejisi
- allow_chat_direct_messages: "Doğrudan sohbet mesajlarına izin ver"
- allow_chat_direct_messages_help: "Etkinleştirilirse izin verilen gruplardaki kullanıcılar bu kişiliğe doğrudan mesaj gönderebilir."
- allow_chat_channel_mentions: "Sohbet kanalından bahsetmeye izin ver"
- allow_chat_channel_mentions_help: "Etkinleştirilirse izin verilen gruplardaki kullanıcılar sohbet kanallarında bu kişilikten bahsedebilir."
- allow_personal_messages: "Kişisel mesajlara izin ver"
- allow_personal_messages_help: "Etkinleştirilirse izin verilen gruplardaki kullanıcılar bu kişiliğe kişisel mesaj gönderebilir."
- allow_topic_mentions: "Konu bahsetmelerine izin ver"
- allow_topic_mentions_help: "Etkinleştirilirse izin verilen gruplardaki kullanıcılar konularda bu kişilikten bahsedebilir."
- force_default_llm: "Her zaman varsayılan dil modelini kullan"
- save: "Kaydet"
- saved: "Kişilik kaydedildi"
- enabled: "Etkin mi?"
- tools: "Etkinleştirilmiş araçlar"
- forced_tools: "Zorlanmış araçlar"
- allowed_groups: "İzin verilen gruplar"
- confirm_delete: "Bu kişiliği silmek istediğinizden emin misiniz?"
- new: "Yeni kişilik"
- no_personas: "Henüz herhangi bir kişilik oluşturmadınız"
- title: "Kişilikler"
- short_title: "Kişiler"
- delete: "Sil"
- temperature: "Sıcaklık"
- temperature_help: "LLM için kullanılacak sıcaklık. Yaratıcılığı artırmak için artırın (model varsayılanını kullanmak için boş bırakın, genellikle 0,0 ila 2,0 arasında bir değer)"
- top_p: "Üst P"
- top_p_help: "LLM için kullanılacak en yüksek P, rastgeleliği artırmak için artırın (model varsayılanını kullanmak için boş bırakın, genellikle 0,0 ila 1,0 arasında bir değer)"
- priority: "Öncelik"
- priority_help: "Öncelikli kişilikler kullanıcılara kişilik listesinin en üstünde gösterilir. Birden fazla kişiliğin önceliği varsa bunlar alfabetik olarak sıralanır."
- tool_options: "Araç seçenekleri"
- rag_conversation_chunks: "Konuşma parçalarını ara"
- rag_conversation_chunks_help: "RAG modeli aramaları için kullanılacak parça sayısı. YZ'nin kullanabileceği bağlam miktarını artırmak için artırın."
- persona_description: "Kişilikler, Discourse forumunuzdaki YZ motorunun davranışını özelleştirmenize olanak tanıyan güçlü bir özelliktir. YZ'nin yanıtlarını ve etkileşimlerini yönlendiren bir \"sistem mesajı\" olarak hareket ederek daha kişiselleştirilmiş ve ilgi çekici bir kullanıcı deneyimi oluşturmaya yardımcı olurlar."
- response_format:
- open_modal: "Düzenle"
- modal:
- key_title: "Anahtar"
- filters:
- reset: "Sıfırla"
- rag:
- options:
- rag_chunk_tokens: "Parça token'ı yükle"
- rag_chunk_tokens_help: "RAG modelindeki her parça için kullanılacak belirteç sayısı. YZ'nin kullanabileceği bağlam miktarını artırmak için artırın. (değiştirmek tüm yüklemeleri yeniden indeksler)"
- rag_chunk_overlap_tokens: "Parça örtüşme token'ı yükle"
- rag_chunk_overlap_tokens_help: "RAG modelinde parçalar arasında üst üste binecek token sayısı. (değiştirme tüm yüklemeleri yeniden indeksler)"
- show_indexing_options: "Yükleme seçeneklerini göster"
- hide_indexing_options: "Yükleme seçeneklerini gizle"
- uploads:
- title: "Yüklemeler"
- button: "Dosya ekle"
- filter: "Yüklemeleri filtrele"
- indexed: "İndekslendi"
- indexing: "İndeksleniyor"
- uploaded: "İndekslenmeye hazır"
- uploading: "Yükleniyor..."
- remove: "Yüklemeyi kaldır"
- tools:
- back: "Geri"
- short_title: "Araçlar"
- export: "Dışa Aktar"
- no_tools: "Henüz herhangi bir araç oluşturmadınız"
- name: "Ad"
- new: "Yeni araç"
- description: "Açıklama"
- description_help: "Dil modeli için aracın amacının net bir açıklaması"
- subheader_description: "Araçlar, kullanıcı tanımlı JavaScript işlevleri ile YZ botlarının yeteneklerini genişletir."
- summary: "Özet"
- summary_help: "Son kullanıcılara gösterilecek araçların amacının özeti"
- script: "Komut Dosyası"
- parameters: "Parametreler"
- save: "Kaydet"
- remove_parameter: "Kaldır"
- parameter_required: "Gerekli"
- parameter_enum: "Numaralandırma"
- parameter_name: "Parametre adı"
- parameter_description: "Parametre açıklaması"
- enum_value: "Numaralandırma değeri"
- add_enum_value: "Numaralandırma değeri ekle"
- edit: "Düzenle"
- test: "Testi çalıştır"
- delete: "Sil"
- saved: "Araç kaydedildi"
- confirm_delete: "Bu aracı silmek istediğinizden emin misiniz?"
- test_modal:
- title: "YZ aracını test edin"
- run: "Testi çalıştır"
- result: "Test sonucu"
- llms:
- short_title: "LLM'ler"
- no_llms: "Henüz LLM yok"
- new: "Yeni model"
- display_name: "Ad"
- name: "Model kimliği"
- provider: "Sağlayıcı"
- tokenizer: "Token'laştırıcı"
- url: "Modeli barındıran hizmetin URL'si"
- api_key: "Modeli barındıran hizmetin API Anahtarı"
- enabled_chat_bot: "YZ bot seçicisine izin verin"
- vision_enabled: "Vizyon etkin"
- ai_bot_user: "YZ botu Kullanıcısı"
- save: "Kaydet"
- edit: "Düzenle"
- saved: "LLM modeli kaydedildi"
- back: "Geri"
- confirm_delete: Bu modeli silmek istediğinizden emin misiniz?
- delete: Sil
- seeded_warning: "Bu model sitenizde önceden yapılandırılmış ve düzenlenemez."
- quotas:
- title: "Kullanım kotaları"
- add_title: "Yeni kota oluşturun"
- group: "Grup"
- max_tokens: "Maks. token sayısı"
- max_usages: "Maksimum kullanım"
- duration: "Süre"
- confirm_delete: "Bu kotayı silmek istediğinizden emin misiniz?"
- add: "Kota ekle"
- durations:
- hour: "1 saat"
- six_hours: "6 saat"
- day: "24 saat"
- week: "7 gün"
- custom: "Özel..."
- hours: "saat"
- max_tokens_help: "Bu gruptaki her kullanıcının belirtilen süre içinde kullanabileceği maksimum token (kelime ve karakter) sayısı. Token'lar, YZ modelleri tarafından metni işlemek için kullanılan birimlerdir; kabaca 1 token = 4 karakter veya bir kelimenin 3/4'ü."
- max_usages_help: "Bu gruptaki her kullanıcının belirtilen süre içinde yapay zekâ modelini kullanabileceği maksimum sayı. Bu kota, grup genelinde paylaşılmaz, bireysel kullanıcı başına izlenir."
- usage:
- ai_bot: "YZ botu"
- ai_helper: "Yardımcı"
- ai_persona: "Kişilik (%{persona})"
- ai_summarization: "Özetle"
- ai_embeddings_semantic_search: "YZ araması"
- ai_spam: "İstenmeyen içerik"
- in_use_warning:
- one: "Bu model şu anda %{settings} tarafından kullanılıyor. Yanlış yapılandırılırsa özellik beklendiği gibi çalışmaz."
- other: "Bu model şu anda şunlar tarafından kullanılıyor: %{settings}. Yanlış yapılandırılırsa özellikler beklendiği gibi çalışmaz. "
- model_description:
- none: "Çoğu dil modeli için çalışan genel ayarlar"
- anthropic-claude-opus-4-0: "Antropic'in en akıllı modeli"
- anthropic-claude-3-5-haiku-latest: "Hızlı ve uygun maliyetli"
- google-gemini-2-5-flash: "Çok modlu muhakeme ile hafif, hızlı ve uygun maliyetli"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "Verimli hafif çok dilli model"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "Güçlü çok amaçlı model"
- mistral-mistral-large-latest: "Mistral'in en güçlü modeli"
- mistral-pixtral-large-latest: "Mistral'in en güçlü görüş yeteneğine sahip modeli"
- preseeded_model_description: "%{model} kullanan önceden yapılandırılmış açık kaynaklı model"
- configured:
- title: "Yapılandırılmış LLM'ler"
- preconfigured_llms: "LLM'nizi seçin"
- preconfigured:
- title_no_llms: "Başlamak için bir şablon seçin"
- title: "Yapılandırılmamış LLM şablonları"
- description: "LLM'ler (Büyük Dil Modelleri) içeriği özetlemek, raporlar oluşturmak, müşteri etkileşimlerini otomatikleştirmek ve forum moderasyonunu ve içgörülerini kolaylaştırmak gibi görevler için optimize edilmiş YZ araçlarıdır"
- fake: "Manuel yapılandırma"
- button: "Kur"
- next:
- title: "İleri"
- tests:
- title: "Testi çalıştır"
- running: "Test çalıştırılıyor..."
- success: "Başarılı!"
- failure: "Modelle iletişim kurmaya çalışmak şu hatayı döndürdü: %{error}"
- hints:
- name: "Hangi modeli kullanacağımızı belirtmek için bunu API çağrısına dâhil ediyoruz"
- vision_enabled: "Etkinleştirilirse YZ görüntüleri anlamaya çalışır. Görmeyi destekleyen kullanılan modele bağlıdır. Anthropic, Google ve OpenAI'ın en son modelleri tarafından desteklenir."
- enabled_chat_bot: "Etkinleştirilirse kullanıcılar YZ botu ile kişisel mesajlar oluştururken bu modeli seçebilir"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "Özel"
- provider_fields:
- access_key_id: "AWS Bedrock erişim anahtarı kimliği"
- region: "AWS Bedrock bölgesi"
- organization: "İsteğe bağlı OpenAI kuruluş kimliği"
- disable_system_prompt: "İstemlerde sistem mesajını devre dışı bırakın"
- enable_native_tool: "Yerel araç desteğini etkinleştirin"
- disable_native_tools: "Yerel araç desteğini devre dışı bırakın (XML tabanlı araçlar kullanın)"
- provider_order: "Sağlayıcı sırası (virgülle ayrılmış liste)"
- provider_quantizations: "Sağlayıcı nicelleştirmelerinin sırası (virgülle ayrılmış liste, ör.: fp16,fp8)"
- disable_streaming: "Akış tamamlamalarını devre dışı bırak (akışı akış dışı taleplere dönüştür)"
- reasoning_effort: "Muhakeme eforu (yalnızca muhakeme modelleri için)"
- related_topics:
- title: "İlgili konular"
- pill: "İlgili"
- ai_helper:
- title: "YZ kullanarak değişiklik önerin"
- description: "Aşağıdaki seçeneklerden birini belirleyin ve YZ size metnin yeni bir versiyonunu önersin."
- selection_hint: "İpucu: Ayrıca, yalnızca onu yeniden yazmak için yardımcıyı açmadan önce metnin bir bölümünü de seçebilirsiniz."
- suggest: "YZ ile öner"
- suggest_errors:
- too_many_tags:
- one: "En fazla sadece %{count} etiketine sahip olabilirsiniz"
- other: "En fazla sadece %{count} etiketine sahip olabilirsiniz"
- no_suggestions: "Mevcut öneri yok"
- missing_content: "Lütfen öneri oluşturmak için biraz içerik girin."
- context_menu:
- trigger: "YZ'ye sor"
- loading: "YZ oluşturuyor"
- cancel: "İptal et"
- confirm: "Onayla"
- discard: "Kapat"
- changes: "Önerilen düzenlemeler"
- custom_prompt:
- title: "Özel istem"
- placeholder: "Özel bir istem girin..."
- submit: "İstem gönder"
- translate_prompt: "%{language} diline çevir"
- post_options_menu:
- trigger: "YZ'ye sor"
- title: "YZ'ye sor"
- loading: "YZ oluşturuyor"
- close: "Bitir"
- copy: "Kopyala"
- copied: "Kopyalandı!"
- cancel: "İptal et"
- insert_footnote: "Dipnot ekle"
- footnote_disabled: "Otomatik ekleme devre dışı bırakıldı, kopyala düğmesine tıklayın ve manuel olarak düzenleyin"
- footnote_credits: "YZ ile açıklama"
- fast_edit:
- suggest_button: "Düzenleme öner"
- thumbnail_suggestions:
- title: "Önerilen küçük resimler"
- select: "Seç"
- selected: "Seçili"
- image_caption:
- button_label: "YZ ile alt yazı"
- generating: "Alt yazı oluşturuluyor..."
- credits: "Alt yazısı YZ tarafından hazırlandı"
- save_caption: "Kaydet"
- automatic_caption_setting: "Otomatik alt yazıyı etkinleştirin"
- automatic_caption_loading: "Görüntülere alt yazı ekleniyor..."
- automatic_caption_dialog:
- prompt: "Bu gönderide alt yazısız görüntüler bulunuyor. Görüntü yüklemelerinde otomatik alt yazıları etkinleştirmek ister misiniz? (Bu daha sonra tercihlerinizden değiştirilebilir)"
- confirm: "Etkinleştir"
- cancel: "Bir daha sorma"
- no_content_error: "Üzerinde YZ eylemleri gerçekleştirmek için önce içerik ekleyin"
- reviewables:
- model_used: "Kullanılan model:"
- accuracy: "Doğruluk:"
- embeddings:
- short_title: "Gömmeler"
- new: "Yeni gömme"
- back: "Geri"
- save: "Kaydet"
- saved: "Gömme yapılandırması kaydedildi"
- delete: "Sil"
- confirm_delete: Bu gömme yapılandırmasını kaldırmak istediğinizden emin misiniz?
- empty: "Henüz yerleştirmeleri ayarlamadınız"
- presets: "Ön ayar seçin..."
- configure_manually: "Manuel olarak yapılandırın"
- edit: "Düzenle"
- seeded_warning: "Bu, sitenizde önceden yapılandırılmış ve düzenlenemez."
- tests:
- title: "Testi çalıştır"
- running: "Test çalıştırılıyor..."
- success: "Başarılı!"
- failure: "Bir gömme oluşturmaya çalışmak şu sonucu verdi: %{error}"
- hints:
- dimensions_warning: "Kaydedildikten sonra bu değer değiştirilemez."
- matryoshka_dimensions: "Verilerin hiyerarşik veya çok katmanlı temsili için kullanılan iç içe yerleştirmelerin boyutunu tanımlar, iç içe geçmiş matruşka bebeklerinin birbirinin içine sığmasına benzer."
- sequence_length: "Gömmeler oluştururken veya bir sorguyu işlerken aynı anda işlenebilecek maksimum belirteç sayısı."
- distance_function: "Gömmeler arasındaki benzerliğin, kosinüs mesafesi (vektörler arasındaki açıyı ölçer) veya negatif iç çarpım (vektör değerlerinin örtüşmesini ölçer) kullanılarak nasıl hesaplanacağını belirler."
- display_name: "Ad"
- provider: "Sağlayıcı"
- url: "Gömme hizmeti URL'si"
- api_key: "Gömme hizmeti API Anahtarı"
- tokenizer: "Token'laştırıcı"
- dimensions: "Gömme boyutları"
- max_sequence_length: "Dizi uzunluğu"
- embed_prompt: "Gömme istemi"
- search_prompt: "Arama istemi"
- matryoshka_dimensions: "Matruşka boyutları"
- distance_function: "Mesafe işlevi"
- distance_functions:
- "<#>": "Negatif iç çarpım"
- <=>: "Kosinüs mesafesi"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "Özel"
- provider_fields:
- model_name: "Model adı"
- semantic_search: "Konular (Semantik)"
- semantic_search_loading: "YZ kullanarak daha fazla sonuç arama"
- semantic_search_results:
- toggle: "YZ kullanılarak bulunan %{count} sonuçları gösteriliyor"
- toggle_hidden: "YZ kullanılarak bulunan %{count} sonuç gizleniyor"
- none: "Üzgünüz, YZ aramamız eşleşen konu bulamadı"
- new: "YZ ile yeni sonuçlar aramaya başlamak için \"ara\" seçeneğine basın"
- unavailable: "YZ sonuçları kullanılamıyor"
- semantic_search_tooltips:
- results_explanation: "Etkinleştirildiğinde, ek YZ arama sonuçları aşağıya eklenir."
- invalid_sort: "YZ sonuçlarını göstermek için arama sonuçları Alaka Düzeyine göre sıralanmalıdır"
- semantic_search_unavailable_tooltip: "YZ sonuçlarını göstermek için arama sonuçları Alaka Düzeyine göre sıralanmalıdır"
- ai_generated_result: "YZ kullanılarak bulunan arama sonucu"
- quick_search:
- suffix: "YZ ile tüm konu ve gönderilerde"
- ai_artifact:
- expand_view_label: "Görünümü genişlet"
- collapse_view_label: "Tam Ekrandan Çık (ESC veya Geri tuşu)"
- click_to_run_label: "Artifact'i çalıştır"
- ai_bot:
- llm: "Model"
- pm_warning: "YZ sohbet robotu mesajları moderatörler tarafından düzenli olarak izlenir."
- cancel_streaming: "Yanıtı durdur"
- default_pm_prefix: "[Adsız YZ botu kişisel mesajı]"
- shortcut_title: "YZ botuyla kişisel mesaj başlatın"
- share: "YZ konuşmasını kopyala"
- conversation_shared: "Konuşma kopyalandı"
- debug_ai: "Ham YZ isteğini ve yanıtını görüntüleyin"
- debug_ai_modal:
- title: "YZ etkileşimini görüntüleyin"
- copy_request: "İsteği kopyala"
- copy_response: "Yanıtı kopyala"
- request_tokens: "İstek token'ları:"
- response_tokens: "Yanıt token'ları:"
- request: "İstek"
- response: "Yanıt"
- next_log: "Sonraki"
- previous_log: "Önceki"
- share_full_topic_modal:
- title: "Konuşmayı herkese açık olarak paylaşın"
- share: "Bağlantıyı paylaşın ve kopyalayın"
- update: "Bağlantıyı güncelleyin ve kopyalayın"
- delete: "Paylaşımı sil"
- share_ai_conversation:
- name: "YZ konuşmasını paylaş"
- title: "Bu YZ konuşmasını herkese açık olarak paylaşın"
- invite_ai_conversation:
- button: "Davet et"
- ai_label: "YZ"
- ai_title: "YZ ile konuşma"
- share_modal:
- title: "YZ konuşmasını kopyala"
- copy: "Kopyala"
- context: "Paylaşılacak etkileşimler:"
- share_tip: "Alternatif olarak, konuşmanın tamamını paylaşabilirsiniz"
- bot_names:
- fake: "Sahte Test Botu"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "Bugün"
- last_7_days: "Son 7 gün"
- last_30_days: "Son 30 gün"
- sentiments:
- dashboard:
- title: "Duyarlılık"
- sentiment_analysis:
- filter_types:
- all: "Hepsi"
- positive: "Pozitif"
- neutral: "Nötr"
- negative: "Negatif"
- group_types:
- category: "Kategori"
- tag: "Etiket"
- table:
- sentiment: "Duyarlılık"
- total_count: "Toplam"
- summarization:
- chat:
- title: "Mesajları özetle"
- description: "İstediğiniz zaman diliminde gönderilen konuşmayı özetlemek için aşağıdaki seçeneklerden birini belirleyin."
- summarize: "Özetle"
- since:
- one: "Son bir saat"
- other: "Son %{count} saat"
- topic:
- title: "Konu özeti"
- close: "Özet panelini kapatın"
- topic_list_layout:
- button:
- compact: "Kompakt"
- expanded: "Genişletilmiş"
- expanded_description: "YZ özetleriyle"
- discobot_discoveries:
- regular_results: "Konular"
- collapse: "Daralt"
- tooltip:
- actions:
- disable: "Devredışı"
- review:
- types:
- reviewable_ai_post:
- title: "YZ Bayraklı gönderi"
- reviewable_ai_chat_message:
- title: "YZ Bayraklı sohbet mesajı"
diff --git a/config/locales/client.ug.yml b/config/locales/client.ug.yml
deleted file mode 100644
index f7f2d3f6..00000000
--- a/config/locales/client.ug.yml
+++ /dev/null
@@ -1,196 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ug:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "تەرتىپى"
- tag:
- label: "بەلگە"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "يوللىغۇچى"
- topic_id:
- label: "تېما كىملىكى"
- title:
- label: "ئاتاق"
- categories:
- label: "سەھىپە"
- tags:
- label: "بەلگە"
- llm_triage:
- fields:
- category:
- label: "سەھىپە"
- tags:
- label: "بەلگە"
- canned_reply:
- label: "جاۋاب"
- discourse_ai:
- features:
- back: "كەينى"
- disabled: "(چەكلەندى)"
- groups: "گۇرۇپپا:"
- no_persona: "تەڭشەلمىدى"
- no_groups: "يوق"
- edit: "تەھرىر"
- expand_list:
- one: "(%{count} تېخىمۇ كۆپ)"
- other: "(%{count} تېخىمۇ كۆپ)"
- collapse_list: "(ئازراق كۆرسەت)"
- filters:
- all: "ھەممىسى"
- reset: "ئەسلىگە قايتۇر"
- search:
- name: "ئىزدە"
- spam:
- name: "ئەخلەت خەت"
- modals:
- select_option: "تاللانما تاللىنىدۇ..."
- spam:
- short_title: "ئەخلەت خەت"
- enable: "قوزغات"
- test_modal:
- result: "نەتىجە"
- spam: "ئەخلەت خەت"
- usage:
- summary: "خۇلاسە"
- username: "ئىشلەتكۈچى ئاتى"
- total_requests: "جەمئىي ئىلتىماس"
- periods:
- custom: "ئىختىيارى..."
- ai_persona:
- back: "كەينى"
- name: "ئىسمى"
- edit: "تەھرىر"
- export: "چىقار"
- description: "چۈشەندۈرۈش"
- user: ئىشلەتكۈچى
- save: "ساقلا"
- enabled: "قوزغىتىلدى؟"
- delete: "ئۆچۈر"
- response_format:
- open_modal: "تەھرىر"
- modal:
- key_title: "ئاچقۇچ"
- filters:
- reset: "ئەسلىگە قايتۇر"
- rag:
- uploads:
- title: "يۈكلەنمە"
- uploading: "يۈكلەۋاتىدۇ..."
- tools:
- back: "كەينى"
- export: "چىقار"
- name: "ئىسمى"
- description: "چۈشەندۈرۈش"
- summary: "خۇلاسە"
- script: "قوليازما"
- save: "ساقلا"
- remove_parameter: "چىقىرىۋەت"
- parameter_required: "زۆرۈر"
- edit: "تەھرىر"
- delete: "ئۆچۈر"
- llms:
- display_name: "ئىسمى"
- save: "ساقلا"
- edit: "تەھرىر"
- back: "كەينى"
- delete: ئۆچۈر
- quotas:
- group: "گۇرۇپپا"
- max_usages: "ئەڭ كۆپ ئىشلىتىلىشى"
- duration: "داۋاملىشىش ۋاقتى"
- durations:
- hour: "1 سائەت"
- six_hours: "6 سائەت"
- day: "24 سائەت"
- week: "7 كۈن"
- custom: "ئىختىيارى..."
- hours: "سائەت"
- usage:
- ai_summarization: "خۇلاسە"
- ai_spam: "ئەخلەت خەت"
- next:
- title: "كېيىنكى"
- tests:
- success: "مۇۋەپپەقىيەتلىك!"
- providers:
- google: "Google"
- fake: "ئىختىيارى"
- ai_helper:
- context_menu:
- cancel: "ۋاز كەچ"
- confirm: "جەزملە"
- discard: "تاشلىۋەت"
- post_options_menu:
- close: "تاقا"
- copy: "كۆچۈر"
- copied: "كۆچۈرۈلدى!"
- cancel: "ۋاز كەچ"
- thumbnail_suggestions:
- select: "تاللا"
- selected: "تاللاندى"
- image_caption:
- save_caption: "ساقلا"
- automatic_caption_dialog:
- confirm: "قوزغات"
- embeddings:
- back: "كەينى"
- save: "ساقلا"
- delete: "ئۆچۈر"
- edit: "تەھرىر"
- tests:
- title: "سىناقنى ئىجرا قىل"
- success: "مۇۋەپپەقىيەتلىك!"
- display_name: "ئىسمى"
- providers:
- google: "Google"
- fake: "ئىختىيارى"
- ai_bot:
- debug_ai_modal:
- request: "ئىلتىماس"
- response: "ئىنكاس"
- next_log: "كېيىنكى"
- previous_log: "ئالدىنقى"
- invite_ai_conversation:
- button: "تەكلىپ"
- share_modal:
- copy: "كۆچۈر"
- conversations:
- today: "بۈگۈن"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "ھەممىسى"
- neutral: "بىتەرەپ"
- group_types:
- category: "سەھىپە"
- tag: "بەلگە"
- table:
- total_count: "جەمئىي"
- summarization:
- chat:
- title: "ئۇچۇرنى خۇلاسىلە"
- description: "تۆۋەندىكى تاللانما تاللانسا لازىملىق ۋاقىت دائىرىسىدە يوللانغان سۆھبەتنى خۇلاسىلەيدۇ."
- summarize: "خۇلاسە"
- since:
- one: "ئاخىرقى %{count} سائەت"
- other: "ئاخىرقى %{count} سائەت"
- discobot_discoveries:
- regular_results: "تېما"
- collapse: "يىغ"
- tooltip:
- actions:
- disable: "چەكلە"
diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml
deleted file mode 100644
index a500b7c8..00000000
--- a/config/locales/client.uk.yml
+++ /dev/null
@@ -1,482 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-uk:
- admin_js:
- admin:
- site_settings:
- categories:
- discourse_ai: "Дискурс ШІ"
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Сортувати за"
- tag:
- label: "Теґ"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "Відправник"
- description: "Користувач, який надсилатиме звіт"
- receivers:
- label: "Отримувачі"
- description: "Користувачі, які отримають звіт (електронні листи будуть надіслані безпосередньо на електронну пошту, імена користувачів будуть надіслані в приватному повідомленні)"
- topic_id:
- label: "ID теми"
- title:
- label: "Назва"
- description: "Назва звіту"
- days:
- label: "Дні"
- description: "Часові рамки звіту"
- offset:
- label: "Зсув"
- description: "Під час тестування ви можете запустити звіт за минулі періоди, використовуйте зсув, щоб почати звіт із більш ранньої дати"
- instructions:
- label: "Інструкції"
- description: "Інструкції, надані до великої мовної моделі"
- sample_size:
- label: "Розмір вибірки"
- description: "Кількість дописів до вибірки для звіту"
- tokens_per_post:
- label: "Токени на пост"
- model:
- label: "Модель"
- description: "LLM для створення звітів"
- categories:
- label: "Розділи"
- description: "Фільтрувати теми лише за цими категоріями"
- tags:
- label: "Теґи"
- description: "Фільтрувати теми лише за цими тегами"
- exclude_tags:
- label: "Виключити теги"
- description: "Виключити теми з цими тегами"
- exclude_categories:
- label: "Виключити категорії"
- description: "Виключити теми з цими категоріями"
- allow_secure_categories:
- label: "Дозволити захищені категорії"
- description: "Дозволити створення звіту для тем у захищених категоріях"
- suppress_notifications:
- label: "Вимкнути сповіщення"
- description: "Приховати сповіщення, які може надсилати звіт шляхом перетворення їх у контент. Це змінить згадки та внутрішні посилання."
- debug_mode:
- label: "Режим налагодження"
- description: "Увімкнути режим налагодження, щоб побачити необроблені вхідні та вихідні дані LLM"
- priority_group:
- label: "Пріоритетна група"
- description: "Визначте пріоритетність контенту з цієї групи у звіті"
- temperature:
- label: "Температура"
- top_p:
- label: "Top P"
- llm_tool_triage:
- fields:
- model:
- label: "Модель"
- llm_triage:
- fields:
- system_prompt:
- label: "Системний запит"
- description: "Підказка, яка буде використана для сортування. Для відповіді використовуйте одне слово, яке ви зможете використовувати для запуску дії"
- max_post_tokens:
- label: "Максимальна кількість токенів публікації"
- description: "Максимальна кількість токенів для сканування за допомогою LLM"
- search_for_text:
- label: "Пошук тексту"
- category:
- label: "Категорія"
- description: "Категорія, яку слід застосувати до теми"
- tags:
- label: "Теґи"
- description: "Теги, які слід застосувати до теми"
- canned_reply:
- label: "Відповідь"
- canned_reply_user:
- label: "Відповідь користувача"
- description: "Ім’я користувача для публікації стандартної відповіді"
- hide_topic:
- label: "Приховати тему"
- flag_type:
- label: "Тип прапора"
- description: "Тип прапорця, який буде застосовано до повідомлення (спам або просто підняти на розгляд)"
- flag_post:
- label: "Поскаржитися"
- description: "Позначає повідомлення (як спам, або для перегляду)"
- model:
- label: "Модель"
- description: "Мовна модель, що використовується для тренування"
- temperature:
- label: "Температура"
- discourse_ai:
- title: "ШІ"
- features:
- back: "Назад"
- disabled: "(вимкнено)"
- groups: "Групи:"
- no_persona: "Не встановлено"
- no_groups: "Немає"
- edit: "Редагувати"
- expand_list:
- one: "(ще %{count})"
- few: "(ще %{count})"
- many: "(ще %{count})"
- other: "(ще %{count})"
- collapse_list: "(показати менше)"
- filters:
- all: "Все"
- reset: "Скинути"
- search:
- name: "Пошук"
- ai_helper:
- proofread: Перевірити текст
- explain: "Пояснити"
- markdown_tables: "Створити таблицю у форматі Markdown"
- spam:
- name: "Спам"
- modals:
- select_option: "Вибрати опцію..."
- spam:
- short_title: "Спам"
- last_seven_days: "Останні 7 днів"
- enable: "Увімкнути"
- test_modal:
- result: "Результат"
- spam: "Спам"
- usage:
- summary: "Підсумок"
- model: "Модель"
- username: "Імʼя користувача"
- total_requests: "Всього"
- request_tokens: "Токени запиту"
- response_tokens: "Токени відповіді"
- periods:
- last_day: "Останні 24 години"
- custom: "Власний…"
- ai_persona:
- ai_tools: "Інструменти"
- tool_strategies:
- all: "Застосувати до всіх відповідей"
- replies:
- one: "Застосувати тільки до першої відповіді"
- few: "Застосувати тільки до перших %{count} відповідей"
- many: "Застосувати тільки до перших %{count} відповідей"
- other: "Застосувати тільки до перших %{count} відповідей"
- back: "Назад"
- name: "Імʼя"
- edit: "Редагувати"
- export: "Експорт"
- description: "Опис"
- no_llm_selected: "Мовну модель не вибрано"
- max_context_posts_help: "Максимальна кількість дописів, які ШІ буде використовувати як контекст для відповіді користувачеві. (за замовчуванням порожньо)"
- vision_enabled: Візуальне розпізнавання увімкнено
- vision_enabled_help: Якщо ввімкнено, ШІ намагатиметься зрозуміти зображення, які користувачі публікують у темі, залежно від моделі, яка використовується для підтримки зору. Підтримується останніми моделями від Anthropic, Google і OpenAI.
- vision_max_pixels: Підтримуваний розмір зображення
- tool_details_help: Показуватиме кінцевим користувачам докладні відомості про те, які інструменти запустила мовна модель.
- mentionable_help: Якщо увімкнено, користувачі в дозволених групах можуть згадувати цього користувача в повідомленнях, ШІ відповідатиме від імені цієї персони.
- user: Користувач
- create_user_help: За бажанням ви можете приєднати користувача до цієї персони. Якщо ви це зробите, ШІ використовуватиме цього користувача, щоб відповідати на запити.
- default_llm_help: Мовна модель за замовчуванням для цієї персони. Обов'язково, якщо ви хочете згадувати персону в публічних публікаціях.
- question_consolidator_llm: Мовна модель для Question Consolidator
- question_consolidator_llm_help: Мовна модель для об’єднання запитань, ви можете вибрати менш потужну модель, щоб заощадити кошти.
- allow_chat_direct_messages_help: "Якщо увімкнено, користувачі з дозволених груп можуть надсилати прямі повідомлення цій персоні."
- allow_chat_channel_mentions_help: "Якщо увімкнено, користувачі в дозволених групах можуть згадувати цю персону в чаті."
- allow_personal_messages_help: "Якщо увімкнено, користувачі з дозволених груп можуть надсилати особисті повідомлення цій персоні."
- allow_topic_mentions_help: "Якщо увімкнено, користувачі в дозволених групах можуть згадувати цю персону в темах."
- save: "Зберегти"
- enabled: "Включено?"
- allowed_groups: "Дозволені групи"
- confirm_delete: "Ви впевнені, що хочете видалити цю персону?"
- title: "Персони"
- short_title: "Персони"
- delete: "Видалити"
- temperature: "Температура"
- top_p: "Top P"
- top_p_help: "Top P параметр для LLM, збільште, щоб збільшити випадковість (залиште порожнім, щоб використовувати модель за замовчуванням, як правило, значення від 0,0 до 1,0)"
- priority: "Пріоритет"
- priority_help: "Пріоритетні персони відображаються користувачам у верхній частині списку персон. Якщо кілька персон мають пріоритет, вони будуть сортовані за алфавітом."
- rag_conversation_chunks_help: "Кількість фрагментів для пошуку за моделлю RAG. Збільшуйте, щоб збільшити кількість контексту, який може використовувати ШІ."
- response_format:
- open_modal: "Редагувати"
- modal:
- key_title: "Ключ"
- list:
- enabled: "AI Bot?"
- filters:
- reset: "Скинути"
- rag:
- options:
- rag_chunk_tokens_help: "Кількість токенів, які будуть використані для кожного фрагмента в моделі RAG. Збільште, щоб збільшити кількість контексту, який може використовувати ШІ. (Зміна призведе до повторної переіндексації всіх завантажень)"
- rag_chunk_overlap_tokens_help: "Кількість токенів для перекриття між фрагментами в моделі RAG. (Зміна буде переіндексовувати всі завантаження)"
- uploads:
- title: "Завантаження"
- filter: "Фільтр завантажень"
- indexed: "Індексовано"
- indexing: "Індексація"
- uploaded: "Готовий до індексації"
- uploading: "Надсилання…"
- remove: "Видалити завантаження"
- tools:
- back: "Назад"
- short_title: "Інструменти"
- export: "Експорт"
- name: "Імʼя"
- description: "Опис"
- description_help: "Чіткий опис призначення інструменту для мовної моделі"
- summary: "Підсумок"
- summary_help: "Короткий опис призначення інструментів для відображення кінцевим користувачам"
- script: "Скрипт"
- parameters: "Параметри"
- save: "Зберегти"
- remove_parameter: "Вилучити"
- parameter_required: "Обов'язкові"
- parameter_enum: "Перерахунок"
- enum_value: "Значення переліку"
- add_enum_value: "Додати значення переліку"
- edit: "Редагувати"
- delete: "Видалити"
- saved: "Інструмент збережено"
- confirm_delete: "Ви впевнені, що хочете видалити цей інструмент?"
- llms:
- short_title: "LLMs"
- no_llms: "Ще немає LLM"
- display_name: "Імʼя"
- name: "ID моделі"
- provider: "Провайдер"
- tokenizer: "Токенізатор"
- url: "URL-адреса сервісу, на якому розміщена модель"
- api_key: "Ключ API сервісу, на якому розміщена модель"
- vision_enabled: "Візуальне розпізнавання увімкнено"
- save: "Зберегти"
- edit: "Редагувати"
- back: "Назад"
- confirm_delete: Ви впевнені, що хочете видалити цей тип?
- delete: Видалити
- seeded_warning: "Ця модель попередньо налаштована на вашому сайті і її неможливо редагувати."
- quotas:
- group: "Група"
- max_usages: "Максимальна кількість використань"
- duration: "Тривалість"
- durations:
- hour: "1 година"
- six_hours: "6 годин"
- day: "24 години"
- week: "7 днів"
- custom: "Власний…"
- hours: "годин"
- usage:
- ai_persona: "Персона (%{persona})"
- ai_summarization: "Підсумок"
- ai_spam: "Спам"
- in_use_warning:
- one: "Ця модель зараз використовується %{settings}. У разі неправильного налаштування функція не працюватиме належним чином."
- few: "Ця модель зараз використовується: %{settings}. У разі неправильного налаштування функція не працюватиме належним чином. "
- many: "Ця модель зараз використовується: %{settings}. У разі неправильного налаштування функція не працюватиме належним чином. "
- other: "Ця модель зараз використовується: %{settings}. У разі неправильного налаштування функція не працюватиме належним чином. "
- model_description:
- none: "Загальні налаштування, що працюють для більшості мовних моделей"
- anthropic-claude-opus-4-0: "Найрозумніша модель Anthropic"
- anthropic-claude-3-5-haiku-latest: "Швидко та економічно ефективно"
- google-gemini-2-5-flash: "Легкий, швидкий та економічний з мультимодальними міркуваннями"
- configured:
- title: "Налаштовані LLM"
- preconfigured_llms: "Виберіть вашу LLM"
- preconfigured:
- title_no_llms: "Виберіть шаблон для початку"
- title: "Неналаштовані шаблони LLM"
- fake: "Ручне налаштування"
- button: "Налаштувати"
- next:
- title: "Далі"
- tests:
- running: "Виконується тест..."
- success: "Успіх!"
- failure: "Спроба зв’язатися з моделлю повернула таку помилку: %{error}"
- hints:
- vision_enabled: "Якщо увімкнено, ШІ намагатиметься зрозуміти зображення. Це залежить від використовуваної моделі, що підтримує зір. Підтримується останніми моделями від Anthropic, Google та OpenAI."
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- fake: "Користувацька"
- provider_fields:
- disable_system_prompt: "Вимкнути системні повідомлення у підказках"
- enable_native_tool: "Увімкнути підтримку рідних інструментів"
- disable_native_tools: "Вимкнути підтримку рідних інструментів (використовувати інструменти на основі XML)"
- related_topics:
- pill: "Пов'язані"
- ai_helper:
- title: "Пропонувати зміни за допомогою ШІ"
- description: "Виберіть один із варіантів нижче, і ШІ запропонує вам нову версію тексту."
- selection_hint: "Підказка: Ви також можете вибрати частину тексту перед відкриттям помічника, щоб переписати лише її."
- suggest: "Підказки від ШІ"
- missing_content: "Будь ласка, введіть деякий контент для створення пропозицій."
- context_menu:
- trigger: "Запитайте ШІ"
- loading: "ШІ генерує"
- cancel: "Скасувати"
- confirm: "Підтвердити"
- discard: "Відкинути"
- custom_prompt:
- placeholder: "Введіть власний запит..."
- translate_prompt: "Перекласти на %{language}"
- post_options_menu:
- trigger: "Запитайте ШІ"
- title: "Запитайте ШІ"
- loading: "ШІ генерує"
- close: "Закрити"
- copy: "Копіювати"
- copied: "Скопійовано!"
- cancel: "Скасувати"
- insert_footnote: "Додати виноску"
- footnote_credits: "Пояснення ШІ"
- thumbnail_suggestions:
- select: "Вибрати"
- selected: "Вибрано"
- image_caption:
- button_label: "Підпис зі ШІ"
- generating: "Створення підпису..."
- credits: "Підпис від ШІ"
- save_caption: "Зберегти"
- automatic_caption_loading: "Підписи до зображень..."
- automatic_caption_dialog:
- confirm: "Увімкнути"
- cancel: "Не запитувати знову"
- no_content_error: "Спочатку додайте вміст, щоб застосувати до нього дії ШІ"
- reviewables:
- model_used: "Використана модель:"
- accuracy: "Точність:"
- embeddings:
- back: "Назад"
- save: "Зберегти"
- delete: "Видалити"
- presets: "Виберіть попереднє налаштування..."
- edit: "Редагувати"
- tests:
- title: "Запустити тест"
- running: "Виконується тест..."
- success: "Успіх!"
- display_name: "Імʼя"
- provider: "Провайдер"
- tokenizer: "Токенізатор"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- CDCK: "CDCK"
- fake: "Користувацька"
- semantic_search: "Теми (семантикв)"
- semantic_search_loading: "Пошук більшої кількості результатів за допомогою ШІ"
- semantic_search_results:
- toggle: "Показати результати %{count} , знайдені за допомогою ШІ"
- toggle_hidden: "Приховати %{count} результатів, знайдених за допомогою ШІ"
- none: "Вибачте, наш пошук ШІ не знайшов відповідних тем"
- ai_generated_result: "Результат пошуку знайдено за допомогою ШІ"
- quick_search:
- suffix: "у всіх темах і повідомленнях з ШІ"
- ai_bot:
- llm: "Модель"
- pm_warning: "Повідомлення чат-ботів ШІ регулярно контролюються модераторами."
- cancel_streaming: "Зупинити відповідь"
- default_pm_prefix: "[ПП від ШІ-бота без назви]"
- shortcut_title: "Почніть листування з AI-ботом"
- share: "Копіювати розмову ШІ"
- conversation_shared: "Бесіду скопійовано"
- debug_ai: "Перегляньте необроблений запит і відповідь ШІ"
- debug_ai_modal:
- title: "Переглянути взаємодію зі ШІ"
- copy_request: "Скопіювати запит"
- copy_response: "Копіювати відповідь"
- request_tokens: "Токени запиту:"
- response_tokens: "Токени відповіді:"
- request: "Запит"
- response: "відповідь"
- next_log: "Далі"
- previous_log: "Попередні"
- share_ai_conversation:
- title: "Поділіться цією бесідою зі ШІ публічно"
- invite_ai_conversation:
- button: "Запрошення"
- ai_label: "ШІ"
- ai_title: "Розмова з ШІ"
- share_modal:
- title: "Копіювати розмову ШІ"
- copy: "Копіювати"
- context: "Взаємодія для спільного використання:"
- share_tip: "Крім того, ви можете поділитися усією розмовою"
- bot_names:
- fake: "Фальшивий тестовий бот"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "Сьогодні"
- last_7_days: "Останні 7 днів"
- last_30_days: "Останні 30 днів"
- sentiments:
- dashboard:
- title: "Почуття"
- sentiment_analysis:
- filter_types:
- all: "Все"
- positive: "Позитивний"
- neutral: "Нейтральна"
- negative: "Негативний"
- group_types:
- category: "Розділ"
- tag: "Теґ"
- table:
- sentiment: "Почуття"
- total_count: "Всього"
- summarization:
- chat:
- title: "Підсумкове повідомлення"
- description: "Виберіть опцію нижче, щоб підсумувати розмову, надіслану протягом визначеного періоду."
- summarize: "Підсумок"
- since:
- one: "Остання година"
- few: "Останні %{count} години"
- many: "Останні %{count} годин"
- other: "Останні %{count} годин"
- topic:
- title: "Підсумок теми"
- close: "Закрити панель підсумків"
- discobot_discoveries:
- regular_results: "Теми"
- collapse: "Згорнути"
- tooltip:
- actions:
- disable: "Вимкнути"
- review:
- types:
- reviewable_ai_post:
- title: "Повідомлення з позначкою ШІ"
- reviewable_ai_chat_message:
- title: "Повідомлення чату з прапорцем ШІ"
diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml
deleted file mode 100644
index 7bc4358b..00000000
--- a/config/locales/client.ur.yml
+++ /dev/null
@@ -1,184 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ur:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "ترتیب بہ"
- tag:
- label: "ٹیگ"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ٹاپک ID"
- title:
- label: "عنوان"
- categories:
- label: "اقسام"
- tags:
- label: "ٹیگز"
- llm_triage:
- fields:
- category:
- label: "زمرہ"
- tags:
- label: "ٹیگز"
- canned_reply:
- label: "جواب"
- discourse_ai:
- features:
- back: "واپس"
- disabled: "(غیر فعال)"
- groups: "گروپس:"
- no_groups: "کوئی نہیں"
- edit: "ترمیم کریں"
- expand_list:
- one: "(%{count} مزید)"
- other: "(%{count} مزید)"
- filters:
- all: "تمام"
- reset: "رِی سَیٹ"
- search:
- name: "تلاش کریں"
- spam:
- name: "سپَیم"
- modals:
- select_option: "ایک آپشن منتخب کریں..."
- spam:
- short_title: "سپَیم"
- last_seven_days: "پچھلے 7 دن"
- enable: "فعال کریں"
- test_modal:
- spam: "سپَیم"
- usage:
- summary: "Summary"
- username: "صارف نام"
- total_requests: "کُل درخواستیں"
- periods:
- last_day: "پچھلے 24 گھنٹے"
- custom: "ترمیم..."
- ai_persona:
- back: "واپس"
- name: "نام"
- edit: "ترمیم کریں"
- export: "برآمد"
- description: "تفصیل"
- user: صارف
- save: "محفوظ کریں"
- enabled: "فعال؟"
- delete: "مٹائیں"
- response_format:
- open_modal: "ترمیم کریں"
- modal:
- key_title: "کی"
- filters:
- reset: "رِی سَیٹ"
- rag:
- uploads:
- title: "اَپ لوڈز"
- uploading: "اَپ لوڈ کیا جا رہا ہے..."
- tools:
- back: "واپس"
- export: "برآمد"
- name: "نام"
- description: "تفصیل"
- summary: "Summary"
- save: "محفوظ کریں"
- remove_parameter: "خارج کریں"
- parameter_required: "درکار"
- edit: "ترمیم کریں"
- delete: "مٹائیں"
- llms:
- display_name: "نام"
- save: "محفوظ کریں"
- edit: "ترمیم کریں"
- back: "واپس"
- delete: مٹائیں
- quotas:
- group: "گروپ"
- max_usages: "زیادہ سے زیادہ استعمال"
- duration: "دورانیہ"
- durations:
- hour: "1 گھنٹہ"
- six_hours: "6 گھنٹے"
- day: "24 گھنٹے"
- custom: "ترمیم..."
- hours: "گھنٹے"
- usage:
- ai_summarization: "خلاصہ"
- ai_spam: "سپَیم"
- next:
- title: "اگلا"
- tests:
- success: "کامیابی!"
- providers:
- google: "گُوگَل"
- fake: "اپنی مرضی کا"
- ai_helper:
- context_menu:
- cancel: "منسوخ"
- confirm: "تصدیق کریں"
- discard: "منسوخ"
- post_options_menu:
- close: "بند کریں"
- copy: "کاپی"
- copied: "کاپی کر لیا!"
- cancel: "منسوخ"
- image_caption:
- save_caption: "محفوظ کریں"
- automatic_caption_dialog:
- confirm: "فعال کریں"
- embeddings:
- back: "واپس"
- save: "محفوظ کریں"
- delete: "مٹائیں"
- edit: "ترمیم کریں"
- tests:
- success: "کامیابی!"
- display_name: "نام"
- providers:
- google: "گُوگَل"
- fake: "اپنی مرضی کا"
- ai_bot:
- debug_ai_modal:
- request: "ریکویسٹ"
- response: "جواب"
- next_log: "اگلا"
- previous_log: "پچھلی"
- invite_ai_conversation:
- button: "دعوت دیں"
- share_modal:
- copy: "کاپی"
- conversations:
- today: "آج"
- last_7_days: "پچھلے 7 دن"
- last_30_days: "پچھلے 30 دن"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "تمام"
- neutral: "نیوٹرل"
- group_types:
- category: "زمرہ"
- tag: "ٹیگ"
- table:
- total_count: "کُل"
- summarization:
- chat:
- summarize: "خلاصہ"
- discobot_discoveries:
- regular_results: "ٹاپک"
- collapse: "بند کریں"
- tooltip:
- actions:
- disable: "غیر فعال کریں"
diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml
deleted file mode 100644
index 6216e0a0..00000000
--- a/config/locales/client.vi.yml
+++ /dev/null
@@ -1,182 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-vi:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "Sắp xếp theo"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "ID Chủ đề"
- title:
- label: "Tiêu đề"
- categories:
- label: "Danh mục"
- tags:
- label: "Thẻ"
- llm_triage:
- fields:
- category:
- label: "Danh mục"
- tags:
- label: "Thẻ"
- canned_reply:
- label: "Trả lời"
- discourse_ai:
- features:
- back: "Quay lại"
- disabled: "(vô hiệu hóa)"
- groups: "Nhóm:"
- no_groups: "Không có gì"
- edit: "Sửa"
- expand_list:
- other: "(%{count} khác)"
- filters:
- all: "All"
- reset: "Cài lại"
- search:
- name: "Tìm kiếm"
- spam:
- name: "Rác"
- modals:
- select_option: "Chọn một tùy Chọn..."
- spam:
- short_title: "Rác"
- last_seven_days: "7 ngày qua"
- enable: "Bật"
- test_modal:
- spam: "Rác"
- usage:
- summary: "Tóm tắt"
- username: "Tên tài khoản"
- total_requests: "Tổng số yêu cầu"
- periods:
- last_day: "24 giờ qua"
- custom: "Tùy chỉnh..."
- ai_persona:
- back: "Quay lại"
- name: "Tên"
- edit: "Sửa"
- export: "Xuất"
- description: "Mô tả"
- user: Người dùng
- save: "Lưu lại"
- enabled: "Kích hoạt"
- delete: "Xóa"
- response_format:
- open_modal: "Sửa"
- modal:
- key_title: "Phím"
- filters:
- reset: "Cài lại"
- rag:
- uploads:
- title: "Tải lên"
- uploading: "Đang đăng "
- tools:
- back: "Quay lại"
- export: "Xuất"
- name: "Tên"
- description: "Mô tả"
- summary: "Tóm tắt"
- save: "Lưu lại"
- remove_parameter: "Xoá"
- parameter_required: "Bắt buộc"
- edit: "Sửa"
- delete: "Xóa"
- llms:
- display_name: "Tên"
- save: "Lưu lại"
- edit: "Sửa"
- back: "Quay lại"
- delete: Xóa
- quotas:
- group: "Nhóm"
- max_usages: "Sử dụng tối đa"
- duration: "Thời lượng"
- durations:
- hour: "1 tiếng"
- six_hours: "6 tiếng"
- day: "24 tiếng"
- week: "7 ngày"
- custom: "Tùy chỉnh..."
- hours: "tiếng"
- usage:
- ai_summarization: "Tóm tắt"
- ai_spam: "Rác"
- next:
- title: "Kế tiếp"
- tests:
- success: "Thành công!"
- providers:
- google: "G"
- fake: "Tùy biến"
- ai_helper:
- context_menu:
- cancel: "Huỷ"
- confirm: "Xác nhận"
- discard: "Hủy bỏ"
- post_options_menu:
- close: "Đóng"
- copy: "Sao chép"
- copied: "Đã sao chép!"
- cancel: "Huỷ"
- thumbnail_suggestions:
- select: "Chọn"
- image_caption:
- save_caption: "Lưu lại"
- automatic_caption_dialog:
- confirm: "Bật"
- embeddings:
- back: "Quay lại"
- save: "Lưu lại"
- delete: "Xóa"
- edit: "Sửa"
- tests:
- success: "Thành công!"
- display_name: "Tên"
- providers:
- google: "G"
- fake: "Tùy biến"
- ai_bot:
- debug_ai_modal:
- request: "Yêu cầu"
- response: "Đáp ứng"
- next_log: "Kế tiếp"
- previous_log: "Trước"
- invite_ai_conversation:
- button: "Mời"
- share_modal:
- copy: "Sao chép"
- conversations:
- today: "Hôm nay"
- last_7_days: "7 ngày qua"
- last_30_days: "30 ngày qua"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "All"
- group_types:
- category: "Danh mục"
- table:
- total_count: "Tổng số"
- summarization:
- chat:
- summarize: "Tóm tắt"
- discobot_discoveries:
- regular_results: "Các chủ đề"
- collapse: "Thu gọn"
- tooltip:
- actions:
- disable: "Tắt"
diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml
deleted file mode 100644
index e9be30f5..00000000
--- a/config/locales/client.zh_CN.yml
+++ /dev/null
@@ -1,704 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-zh_CN:
- admin_js:
- admin:
- api:
- scopes:
- descriptions:
- discourse_ai:
- search: "允许 AI 搜索"
- stream_completion: "允许流式 AI 角色补全"
- update_personas: "允许更新 AI 角色模型"
- site_settings:
- categories:
- discourse_ai: "Discourse AI"
- dashboard:
- emotion:
- title: "情绪"
- description: "该表列出了按确定的情绪分类的帖子数量。使用“SamLowe/roberta-base-go_emotions”模型进行分类。"
- reports:
- filters:
- group_by:
- label: "按…分组"
- sort_by:
- label: "排序依据"
- tag:
- label: "标签"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- sender:
- label: "发送人"
- description: "将发送报告的用户"
- receivers:
- label: "接收人"
- description: "将收到报告的用户(电子邮件地址将收到直接电子邮件,用户名将收到私信)"
- topic_id:
- label: "话题 ID"
- description: "要将报告发布到的话题 ID"
- title:
- label: "标题"
- description: "报告的标题"
- days:
- label: "天数"
- description: "报告的时间范围"
- offset:
- label: "偏移"
- description: "在测试时,您可能希望运行历史报告,请使用“偏移”启动较早日期的报告"
- instructions:
- label: "指令"
- description: "提供给大语言模型的指令"
- sample_size:
- label: "样本大小"
- description: "要为报告抽样的帖子数"
- tokens_per_post:
- label: "每个帖子的词元数"
- description: "每个帖子要使用的 LLM 词元数"
- model:
- label: "模型"
- description: "用于生成报告的 LLM"
- categories:
- label: "类别"
- description: "仅筛选这些类别的话题"
- tags:
- label: "标签"
- description: "仅筛选这些标签的话题"
- exclude_tags:
- label: "排除标签"
- description: "排除带有这些标签的话题"
- exclude_categories:
- label: "排除类别"
- description: "排除这些类别的话题"
- allow_secure_categories:
- label: "允许安全类别"
- description: "允许为安全类别中的话题生成报告"
- suppress_notifications:
- label: "禁止通知"
- description: "通过转换为内容来禁止报告可能生成的通知。这将重新映射提及和内部链接。"
- debug_mode:
- label: "调试模式"
- description: "启用调试模式以查看 LLM 的原始输入和输出"
- priority_group:
- label: "优先群组"
- description: "在报告中优先考虑来自此群组的内容"
- temperature:
- label: "温度"
- description: "用于 LLM 的温度值,增加该值会提高生成内容的随机性(留空则使用模型默认值)"
- top_p:
- label: "Top P"
- description: "用于 LLM 的 Top P 值,增加该值会提高生成内容的随机性(留空则使用模型默认值)"
- llm_tool_triage:
- fields:
- model:
- label: "模型"
- description: "用于初步分类的默认语言模型"
- tool:
- label: "工具"
- description: "用于初步分类的工具(工具不得包含已定义的参数)"
- llm_persona_triage:
- fields:
- persona:
- label: "角色模型"
- description: "用于初步分类的 AI 角色模型(必须设置默认 LLM 和用户)"
- whisper:
- label: "以 Whisper 模式回复"
- llm_triage:
- fields:
- system_prompt:
- label: "系统提示"
- description: "将用于分类的提示,请确保它回复一个您可用于触发操作的字词"
- max_post_tokens:
- label: "帖子最大词元数"
- description: "使用 LLM 分类扫描的最大词元数"
- stop_sequences:
- label: "停止序列"
- description: "指示模型在达到其中一个值时停止生成词元"
- search_for_text:
- label: "搜索文本"
- description: "如果 LLM 回复中出现以下文本,则应用这些操作"
- category:
- label: "类别"
- description: "应用于话题的类别"
- tags:
- label: "标签"
- description: "应用于话题的标签"
- canned_reply:
- label: "回复"
- description: "要为话题发布的预设回复的原始文本"
- canned_reply_user:
- label: "回复用户"
- description: "发布预设回复的用户的用户名"
- hide_topic:
- label: "隐藏话题"
- description: "如果触发,则将话题设为对公众不可见"
- flag_type:
- label: "举报类型"
- description: "对帖子应用的举报类型(垃圾内容或只是提出审核请求)"
- flag_post:
- label: "举报帖子"
- description: "举报帖子(垃圾内容或请求审核)"
- include_personal_messages:
- label: "包括私信"
- description: "同时扫描私信并对其进行分类"
- model:
- label: "模型"
- description: "用于分类的语言模型"
- temperature:
- label: "温度"
- discourse_ai:
- title: "AI"
- features:
- back: "返回"
- disabled: "(已禁用)"
- persona:
- other: "角色:"
- groups: "群组:"
- llm:
- other: "LLM:"
- no_persona: "未设置"
- no_groups: "无"
- edit: "编辑"
- expand_list:
- other: "(%{count})"
- collapse_list: "(收起)"
- filters:
- all: "所有"
- reset: "重置"
- search:
- name: "搜索"
- embeddings:
- name: "嵌入向量"
- ai_helper:
- name: "助手"
- proofread: 审校文本
- explain: "解释"
- smart_dates: "智能日期"
- markdown_tables: "生成 Markdown 表"
- custom_prompt: "自定义提示"
- spam:
- name: "垃圾内容"
- description: "使用所选 LLM 识别潜在垃圾内容,并进行标记,使其进入审核队列,以便网站版主可以进行检查"
- modals:
- select_option: "选择一个选项…"
- spam:
- short_title: "垃圾内容"
- title: "配置垃圾内容处理"
- select_llm: "选择 LLM"
- custom_instructions: "自定义指令"
- custom_instructions_help: "特定于您的网站的自定义指令,帮助指导 AI 识别垃圾内容,例如“更积极地扫描非英语帖子”。"
- last_seven_days: "过去 7 天"
- scanned_count: "已扫描的帖子"
- false_positives: "错误举报"
- false_negatives: "错过的垃圾内容"
- spam_detected: "检测到垃圾内容"
- custom_instructions_placeholder: "针对特定网站的指令,帮助 AI 更准确地识别垃圾内容"
- enable: "启用"
- spam_tip: "AI 垃圾内容检测功能会扫描所有新用户在公开话题中发布的前 3 个帖子。如果这些帖子可能是垃圾内容,它会标记这些帖子以供审核并屏蔽这些用户。"
- settings_saved: "设置已保存"
- spam_description: "使用所选 LLM 识别潜在垃圾内容,并进行标记,使其进入审核队列,以便网站版主可以进行检查"
- no_llms: "无可用 LLM"
- test_button: "测试…"
- save_button: "保存更改"
- test_modal:
- title: "测试垃圾内容检测"
- post_url_label: "帖子 URL 或 ID"
- post_url_placeholder: "https://your-forum.com/t/topic/123/4 或帖子 ID"
- result: "结果"
- scan_log: "扫描日志"
- run: "运行测试"
- spam: "垃圾内容"
- not_spam: "非垃圾内容"
- stat_tooltips:
- incorrectly_flagged: "AI 机器人标记为垃圾内容但版主不同意的条目"
- missed_spam: "被社区标记为垃圾内容但未被 AI 机器人检测到且版主认为是垃圾内容的条目"
- errors:
- scan_not_admin:
- message: "警告:垃圾内容扫描无法正常工作,因为垃圾内容扫描帐户不是管理员帐户"
- action: "修复"
- resolved: "错误已解决!"
- usage:
- short_title: "使用量"
- summary: "摘要"
- total_tokens: "词元总数"
- tokens_over_time: "一段时间内的词元数"
- features_breakdown: "按功能的使用量"
- feature: "功能"
- usage_count: "使用次数"
- model: "模型"
- models_breakdown: "按模型的使用量"
- users_breakdown: "按用户的使用量"
- all_features: "所有功能"
- all_models: "所有模型"
- username: "用户名"
- total_requests: "请求总数"
- request_tokens: "请求词元"
- response_tokens: "回答词元"
- net_request_tokens: "网络请求词元"
- cached_tokens: "缓存的词元"
- cached_request_tokens: "缓存的请求词元"
- no_users: "找不到用户使用数据"
- no_models: "找不到模型使用数据"
- no_features: "找不到功能使用数据"
- stat_tooltips:
- total_requests: "通过 Discourse 向 LLM 提出的所有请求"
- total_tokens: "提示 LLM 时使用的所有词元"
- request_tokens: "LLM 试图理解您所说的话时使用的词元"
- response_tokens: "LLM 回答您的提示时使用的词元"
- cached_tokens: "先前处理过的请求词元,LLM 会重复使用这些词元优化性能和成本"
- periods:
- last_day: "过去 24 小时"
- last_week: "上周"
- last_month: "上个月"
- custom: "自定义…"
- ai_persona:
- ai_tools: "工具"
- tool_strategies:
- all: "应用于所有回复"
- replies:
- other: "应用于前 %{count} 条回复"
- back: "返回"
- name: "名称"
- edit: "编辑"
- export: "导出"
- description: "描述"
- no_llm_selected: "未选择语言模型"
- max_context_posts: "最大上下文帖子数"
- max_context_posts_help: "AI 在回复用户时用作上下文的最大帖子数。(默认为空)"
- vision_enabled: 启用视觉
- vision_enabled_help: 如果启用,AI 将尝试理解用户在话题中发布的图片,这取决于使用的模型是否支持视觉。Anthropic、Google 和 OpenAI 的最新模型支持该功能。
- vision_max_pixels: 支持的图片大小
- vision_max_pixel_sizes:
- low: 低品质 - 最便宜 (256x256)
- medium: 中等品质 (512x512)
- high: 高品质 - 最慢 (1024x1024)
- tool_details: 显示工具详细信息
- tool_details_help: 将向最终用户显示语言模型触发了哪些工具的详细信息。
- mentionable: 允许提及
- mentionable_help: 如果启用,允许的群组中的用户可以在帖子中提及此用户,AI 将以此角色的身份进行回复。
- user: 用户
- create_user: 创建用户
- create_user_help: 您可以选择为此角色附加一个用户。如果这样做,AI 将使用此用户来回复请求。
- default_llm: 默认语言模型
- default_llm_help: 用于此角色的默认语言模型。如果您希望在公开帖子中提及该角色,则为必选项。
- question_consolidator_llm: 问题整合器的语言模型
- question_consolidator_llm_help: 用于问题整合器的语言模型,您可以选择功能较弱的模型来节省成本。
- system_prompt: 系统提示
- forced_tool_strategy: 强制工具策略
- allow_chat_direct_messages: "允许聊天直接消息"
- allow_chat_direct_messages_help: "如果启用,允许的群组中的用户可以向此角色发送直接消息。"
- allow_chat_channel_mentions: "允许聊天频道提及"
- allow_chat_channel_mentions_help: "如果启用,允许的群组中的用户可以在聊天频道中提及此角色。"
- allow_personal_messages: "允许私信"
- allow_personal_messages_help: "如果启用,允许的群组中的用户可以向此角色发送私信。"
- allow_topic_mentions: "允许话题提及"
- allow_topic_mentions_help: "如果启用,允许的群组中的用户可以在话题中提及此角色。"
- force_default_llm: "始终使用默认语言模型"
- save: "保存"
- saved: "角色已保存"
- enabled: "已启用?"
- tools: "启用的工具"
- forced_tools: "强制工具"
- allowed_groups: "允许的群组"
- confirm_delete: "确定要删除此角色吗?"
- new: "新角色"
- no_personas: "您还没有创建任何角色"
- title: "角色"
- short_title: "角色"
- delete: "删除"
- temperature: "温度"
- temperature_help: "用于 LLM 的温度。增大该值可以提升创造力(留空将使用模型默认值,通常为 0.0 到 2.0 之间的值)"
- top_p: "Top P"
- top_p_help: "用于 LLM 的 Top P,增大可以提升随机性(留空将使用模型默认值,通常为 0.0 到 1.0 之间的值)"
- priority: "优先"
- priority_help: "优先角色会在角色列表的顶部向用户显示。如果多个角色都具有优先级,将按字母顺序排序。"
- tool_options: "工具选项"
- rag_conversation_chunks: "搜索对话分块"
- rag_conversation_chunks_help: "为 RAG 模型搜索使用的分块数。增加分块数会增加 AI 可以使用的上下文数量。"
- persona_description: "角色是一种强大的功能,可以让您自定义 Discourse 论坛中 AI 引擎的行为。它们充当“系统消息”,指导 AI 的回答和互动,帮助创造更加个性化、引人入胜的用户体验。"
- response_format:
- open_modal: "编辑"
- modal:
- key_title: "键"
- list:
- enabled: "AI 机器人?"
- ai_bot:
- title: "AI 机器人选项"
- filters:
- reset: "重置"
- rag:
- options:
- rag_chunk_tokens: "上传分块词元"
- rag_chunk_tokens_help: "RAG 模型中为每个分块使用的词元数。增大词元数会增加 AI 可以使用的上下文数量。(更改词元数将为所有上传内容重新编制索引)"
- rag_chunk_overlap_tokens: "上传分块重叠词元"
- rag_chunk_overlap_tokens_help: "RAG 模型中分块之间重叠的词元数。(更改词元数将为所有上传内容重新编制索引)"
- show_indexing_options: "显示上传选项"
- hide_indexing_options: "隐藏上传选项"
- uploads:
- title: "上传"
- button: "添加文件"
- filter: "筛选上传"
- indexed: "已编制索引"
- indexing: "正在编制索引"
- uploaded: "准备好编制索引"
- uploading: "正在上传…"
- remove: "移除上传"
- tools:
- back: "返回"
- short_title: "工具"
- export: "导出"
- no_tools: "您还没有创建任何工具"
- name: "名称"
- new: "新工具"
- description: "描述"
- description_help: "向语言模型介绍工具用途的清晰描述"
- subheader_description: "工具可以通过用户定义的 JavaScript 函数扩展 AI 机器人的功能。"
- summary: "摘要"
- summary_help: "显示给最终用户的工具用途摘要"
- script: "脚本"
- parameters: "参数"
- save: "保存"
- remove_parameter: "移除"
- parameter_required: "必选"
- parameter_enum: "枚举"
- parameter_name: "参数名称"
- parameter_description: "参数描述"
- enum_value: "枚举值"
- add_enum_value: "添加枚举值"
- edit: "编辑"
- test: "运行测试"
- delete: "删除"
- saved: "工具已保存"
- confirm_delete: "确定要删除此工具吗?"
- test_modal:
- title: "测试 AI 工具"
- run: "运行测试"
- result: "测试结果"
- llms:
- short_title: "LLM"
- no_llms: "没有 LLM"
- new: "新模型"
- display_name: "名称"
- name: "模型 ID"
- provider: "提供程序"
- tokenizer: "词元生成器"
- url: "托管模型的服务的 URL"
- api_key: "托管模型的服务的 API 密钥"
- enabled_chat_bot: "允许 AI 机器人选择器"
- vision_enabled: "启用视觉"
- ai_bot_user: "AI 机器人用户"
- save: "保存"
- edit: "编辑"
- saved: "LLM 模型已保存"
- back: "返回"
- confirm_delete: 确定要删除此模型吗?
- delete: 删除
- seeded_warning: "此模型已在您的网站上预先配置,无法编辑。"
- quotas:
- title: "使用配额"
- add_title: "创建新配额"
- group: "群组"
- max_tokens: "最大词元数"
- max_usages: "最大使用次数"
- duration: "持续时间"
- confirm_delete: "确定要删除此配额吗?"
- add: "添加配额"
- durations:
- hour: "1 小时"
- six_hours: "6 小时"
- day: "24 小时"
- week: "7 天"
- custom: "自定义…"
- hours: "小时"
- max_tokens_help: "此群组中每个用户在指定时长内可以使用的最大词元(单词和字符)数。词元是 AI 模型处理文本时使用的单位,1 个词元约等于 4 个字符或 3/4 个单词。"
- max_usages_help: "此群组中每个用户在指定时长内可以使用 AI 模型的最大次数。此配额按个别用户进行跟踪,不在整个群组中共享。"
- usage:
- ai_bot: "AI 机器人"
- ai_helper: "助手"
- ai_persona: "角色 (%{persona})"
- ai_summarization: "总结"
- ai_embeddings_semantic_search: "AI 搜索"
- ai_spam: "垃圾内容"
- in_use_warning:
- other: "此模型目前的使用者为 %{settings}。如果配置错误,功能将无法按预期运行。"
- model_description:
- none: "适用于大多数语言模型的常规设置"
- anthropic-claude-opus-4-0: "Anthropic 最聪明的模型"
- anthropic-claude-3-5-haiku-latest: "快速、经济实惠"
- google-gemini-2-5-flash: "轻量、快速、经济高效,具有多模态推理能力"
- samba_nova-Meta-Llama-3-1-8B-Instruct: "高效的轻量级多语言模型"
- samba_nova-Meta-Llama-3-3-70B-Instruct": "强大的多用途模型"
- mistral-mistral-large-latest: "Mistral 最强大的模型"
- mistral-pixtral-large-latest: "Mistral 最强大的视觉能力模型"
- preseeded_model_description: "利用 %{model} 的预配置开源模型"
- configured:
- title: "配置的 LLM"
- preconfigured_llms: "选择您的 LLM"
- preconfigured:
- title_no_llms: "选择一个模板以开始"
- title: "未配置的 LLM 模板"
- fake: "手动配置"
- button: "设置"
- next:
- title: "下一步"
- tests:
- title: "运行测试"
- running: "正在运行测试…"
- success: "成功!"
- failure: "尝试联系模型时返回此错误:%{error}"
- hints:
- name: "我们将其包含在 API 调用中以指定我们将使用哪个模型"
- vision_enabled: "如果启用,AI 将尝试理解图片,这取决于使用的模型是否支持视觉。Anthropic、Google 和 OpenAI 的最新模型支持该功能。"
- enabled_chat_bot: "如果启用,用户可以在使用 AI 机器人创建私信时选择此模型"
- providers:
- aws_bedrock: "AWS Bedrock"
- anthropic: "Anthropic"
- vllm: "vLLM"
- hugging_face: "Hugging Face"
- cohere: "Cohere"
- open_ai: "OpenAI"
- google: "Google"
- azure: "Azure"
- ollama: "Ollama"
- CDCK: "CDCK"
- samba_nova: "SambaNova"
- mistral: "Mistral"
- open_router: "OpenRouter"
- fake: "自定义"
- provider_fields:
- access_key_id: "AWS Bedrock 访问密钥 ID"
- region: "AWS Bedrock 区域"
- organization: "可选 OpenAI 组织 ID"
- disable_system_prompt: "在提示中禁用系统消息"
- enable_native_tool: "启用原生工具支持"
- disable_native_tools: "禁用原生工具支持(使用基于 XML 的工具)"
- provider_order: "提供程序顺序(逗号分隔列表)"
- provider_quantizations: "提供程序量化顺序(逗号分隔列表,例如:fp16,fp8)"
- disable_streaming: "禁用流式补全(将流式请求转换为非流式请求)"
- related_topics:
- title: "相关话题"
- pill: "相关"
- ai_helper:
- title: "使用 AI 提出更改建议"
- description: "选择以下选项之一,AI 将向您推荐新版本文本。"
- selection_hint: "提示:您也可以在打开助手之前选择文本的一部分来仅重写该文本。"
- suggest: "通过 AI 提出建议"
- suggest_errors:
- too_many_tags:
- other: "您最多只能有 %{count} 个标签"
- no_suggestions: "没有建议"
- missing_content: "请输入一些内容以生成建议。"
- context_menu:
- trigger: "询问 AI"
- loading: "AI 正在生成"
- cancel: "取消"
- confirm: "确认"
- discard: "舍弃"
- changes: "建议的编辑"
- custom_prompt:
- title: "自定义提示"
- placeholder: "输入自定义提示…"
- submit: "发送提示"
- translate_prompt: "翻译为%{language}"
- post_options_menu:
- trigger: "询问 AI"
- title: "询问 AI"
- loading: "AI 正在生成"
- close: "关闭"
- copy: "复制"
- copied: "已复制!"
- cancel: "取消"
- insert_footnote: "添加脚注"
- footnote_disabled: "自动插入功能已禁用,请点击“复制”按钮并手动编辑"
- footnote_credits: "AI 的解释"
- fast_edit:
- suggest_button: "建议编辑"
- thumbnail_suggestions:
- title: "建议的缩略图"
- select: "选择"
- selected: "已选择"
- image_caption:
- button_label: "使用 AI 生成标题"
- generating: "正在生成标题…"
- credits: "由 AI 生成标题"
- save_caption: "保存"
- automatic_caption_setting: "启用自动标题"
- automatic_caption_loading: "正在为图片生成标题…"
- automatic_caption_dialog:
- prompt: "此帖子包含无标题图片。要在上传图片时启用自动生成标题功能吗?(稍后可以在偏好设置中进行更改)"
- confirm: "启用"
- cancel: "不再询问"
- no_content_error: "先添加内容,然后对其执行 AI 操作"
- reviewables:
- model_used: "使用的模型:"
- accuracy: "准确性:"
- embeddings:
- short_title: "嵌入向量"
- new: "新建嵌入向量"
- back: "返回"
- save: "保存"
- saved: "嵌入向量配置已保存"
- delete: "删除"
- confirm_delete: 确定要移除此嵌入向量配置吗?
- empty: "您尚未设置嵌入向量"
- presets: "选择一个预设…"
- configure_manually: "手动配置"
- edit: "编辑"
- seeded_warning: "此内容在您的网站上预先配置,无法编辑。"
- tests:
- title: "运行测试"
- running: "正在运行测试…"
- success: "成功!"
- failure: "尝试生成嵌入向量导致以下错误:%{error}"
- hints:
- dimensions_warning: "保存后,此值将无法更改。"
- matryoshka_dimensions: "定义用于分层或多层数据表示的嵌套嵌入向量的大小,类似于套娃相互契合的方式。"
- sequence_length: "创建嵌入向量或处理查询时一次可以处理的最大词元数。"
- distance_function: "确定如何计算嵌入向量之间的相似度,可以使用余弦距离(测量向量的夹角)或负内积(测量向量值的重叠)。"
- display_name: "名称"
- provider: "提供程序"
- url: "嵌入向量服务 URL"
- api_key: "嵌入向量服务 API 密钥"
- tokenizer: "分词器"
- dimensions: "嵌入向量尺寸"
- max_sequence_length: "序列长度"
- embed_prompt: "嵌入提示"
- search_prompt: "搜索提示"
- matryoshka_dimensions: "套娃尺寸"
- distance_function: "距离函数"
- distance_functions:
- "<#>": "负内积"
- <=>: "余弦距离"
- providers:
- hugging_face: "Hugging Face"
- open_ai: "OpenAI"
- google: "Google"
- cloudflare: "Cloudflare"
- CDCK: "CDCK"
- fake: "自定义"
- provider_fields:
- model_name: "模型名称"
- semantic_search: "话题(语义)"
- semantic_search_loading: "正在使用 AI 搜索更多结果"
- semantic_search_results:
- toggle: "显示使用 AI 找到的 %{count} 个结果"
- toggle_hidden: "隐藏使用 AI 找到的 %{count} 个结果"
- none: "抱歉,我们的 AI 搜索没有找到匹配的话题"
- new: "按“搜索”开始使用 AI 查找新结果"
- unavailable: "AI 结果不可用"
- semantic_search_tooltips:
- results_explanation: "启用后,将在下方添加更多 AI 搜索结果。"
- invalid_sort: "搜索结果必须按相关性排序才能显示 AI 结果"
- semantic_search_unavailable_tooltip: "搜索结果必须按相关性排序才能显示 AI 结果"
- ai_generated_result: "使用 AI 找到的搜索结果"
- quick_search:
- suffix: "在所有话题和帖子中使用 AI 搜索"
- ai_artifact:
- expand_view_label: "扩展视图"
- collapse_view_label: "退出全屏(按 ESC 或“返回”按钮)"
- click_to_run_label: "运行工件"
- ai_bot:
- persona: "角色模型"
- llm: "模型"
- pm_warning: "版主会定期监控 AI 聊天机器人消息。"
- cancel_streaming: "停止回复"
- default_pm_prefix: "[无标题 AI 机器人私信]"
- shortcut_title: "使用 AI 机器人启动私信"
- share: "复制 AI 对话"
- conversation_shared: "对话已复制"
- debug_ai: "查看原始 AI 请求和回答"
- debug_ai_modal:
- title: "查看 AI 交互"
- copy_request: "复制请求"
- copy_response: "复制回答"
- request_tokens: "请求令牌:"
- response_tokens: "回答令牌:"
- request: "请求"
- response: "回答"
- next_log: "下一步"
- previous_log: "上一步"
- share_full_topic_modal:
- title: "公开分享对话"
- share: "分享并复制链接"
- update: "更新并复制链接"
- delete: "删除分享"
- share_ai_conversation:
- name: "分享 AI 对话"
- title: "公开分享此 AI 对话"
- invite_ai_conversation:
- button: "邀请"
- ai_label: "AI"
- ai_title: "与 AI 的对话"
- share_modal:
- title: "复制 AI 对话"
- copy: "复制"
- context: "要分享的互动:"
- share_tip: "或者,您可以分享整个对话"
- bot_names:
- fake: "假测试机器人"
- claude-3-opus: "Claude 3 Opus"
- claude-3-sonnet: "Claude 3 Sonnet"
- claude-3-haiku: "Claude 3 Haiku"
- cohere-command-r-plus: "Cohere Command R Plus"
- gpt-4: "GPT-4"
- gpt-4-turbo: "GPT-4 Turbo"
- gpt-4o: "GPT-4 Omni"
- gpt-3:
- 5-turbo: "GPT-3.5"
- claude-2: "Claude 2"
- gemini-1:
- 5-pro: "Gemini"
- mixtral-8x7B-Instruct-V0:
- "1": "Mixtral-8x7B V0.1"
- conversations:
- today: "今天"
- last_7_days: "过去 7 天"
- last_30_days: "过去 30 天"
- sentiments:
- dashboard:
- title: "情绪"
- sentiment_analysis:
- filter_types:
- all: "所有"
- positive: "积极"
- neutral: "中性"
- negative: "消极"
- group_types:
- category: "类别"
- tag: "标签"
- table:
- sentiment: "情绪"
- total_count: "总计"
- summarization:
- chat:
- title: "总结消息"
- description: "选择以下选项,以总结在所需时间范围内发送的对话。"
- summarize: "总结"
- since:
- other: "过去 %{count} 小时"
- topic:
- title: "话题摘要"
- close: "关闭摘要面板"
- topic_list_layout:
- button:
- compact: "紧凑"
- expanded: "展开"
- expanded_description: "带 AI 摘要"
- discobot_discoveries:
- regular_results: "话题"
- collapse: "收起"
- tooltip:
- actions:
- disable: "禁用"
- review:
- types:
- reviewable_ai_post:
- title: "AI 举报的帖子"
- reviewable_ai_chat_message:
- title: "AI 举报的聊天消息"
diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml
deleted file mode 100644
index bd758e26..00000000
--- a/config/locales/client.zh_TW.yml
+++ /dev/null
@@ -1,187 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-zh_TW:
- admin_js:
- admin:
- dashboard:
- reports:
- filters:
- sort_by:
- label: "排序"
- tag:
- label: "標記"
- js:
- discourse_automation:
- scriptables:
- llm_report:
- fields:
- topic_id:
- label: "話題 ID"
- title:
- label: "標題"
- categories:
- label: "分類"
- tags:
- label: "標記"
- llm_triage:
- fields:
- category:
- label: "分類"
- tags:
- label: "標記"
- canned_reply:
- label: "回覆"
- discourse_ai:
- features:
- back: "上一步"
- disabled: "(停用)"
- groups: "群組:"
- no_persona: "未設定"
- no_groups: "無"
- edit: "編輯"
- expand_list:
- other: "(%{count} 更多)"
- collapse_list: "(顯示更少)"
- filters:
- all: "全部"
- reset: "重置"
- search:
- name: "搜尋"
- spam:
- name: "垃圾內容"
- spam:
- short_title: "垃圾內容"
- last_seven_days: "過去 7 天"
- enable: "啟用"
- test_modal:
- spam: "垃圾內容"
- usage:
- summary: "摘要"
- username: "使用者名稱"
- total_requests: "請求總數"
- periods:
- last_day: "過去 24 小時"
- custom: "自訂..."
- ai_persona:
- back: "上一步"
- name: "名字"
- edit: "編輯"
- export: "匯出"
- description: "簡述"
- user: 使用者
- save: "保存"
- enabled: "啟用?"
- delete: "刪除"
- response_format:
- open_modal: "編輯"
- modal:
- key_title: "金鑰"
- filters:
- reset: "重置"
- rag:
- uploads:
- title: "上傳檔案"
- uploading: "上傳中..."
- tools:
- back: "上一步"
- export: "匯出"
- name: "名字"
- description: "簡述"
- summary: "摘要"
- save: "保存"
- remove_parameter: "移除"
- parameter_required: "必要設定"
- edit: "編輯"
- delete: "刪除"
- llms:
- display_name: "名字"
- save: "保存"
- edit: "編輯"
- back: "上一步"
- delete: 刪除
- quotas:
- group: "群組"
- duration: "持續時間"
- durations:
- hour: "1 小時"
- six_hours: "6 小時"
- day: "24 小時"
- custom: "自訂..."
- hours: "小時"
- usage:
- ai_summarization: "總結"
- ai_spam: "垃圾內容"
- next:
- title: "下一步"
- tests:
- success: "成功!"
- providers:
- google: "Google"
- fake: "客製"
- ai_helper:
- context_menu:
- cancel: "取消"
- discard: "捨棄"
- post_options_menu:
- close: "關閉"
- copy: "複製"
- copied: "已複製!"
- cancel: "取消"
- thumbnail_suggestions:
- select: "選擇"
- selected: "已選"
- image_caption:
- save_caption: "保存"
- automatic_caption_dialog:
- confirm: "啟用"
- embeddings:
- back: "上一步"
- save: "保存"
- delete: "刪除"
- edit: "編輯"
- tests:
- success: "成功!"
- display_name: "名字"
- providers:
- google: "Google"
- fake: "客製"
- ai_bot:
- debug_ai_modal:
- request: "請求"
- response: "回應"
- next_log: "下一步"
- previous_log: "舊"
- invite_ai_conversation:
- button: "邀請"
- share_modal:
- copy: "複製"
- conversations:
- today: "今天"
- last_7_days: "過去 7 天"
- last_30_days: "過去 30 天"
- sentiments:
- sentiment_analysis:
- filter_types:
- all: "全部"
- neutral: "中性"
- group_types:
- category: "分類"
- tag: "標記"
- table:
- total_count: "總數"
- summarization:
- chat:
- title: "摘要訊息"
- summarize: "總結"
- since:
- other: "前 %{count} 小時"
- discobot_discoveries:
- regular_results: "話題"
- collapse: "收起"
- tooltip:
- actions:
- disable: "禁用"
diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml
deleted file mode 100644
index 9236d20d..00000000
--- a/config/locales/server.ar.yml
+++ /dev/null
@@ -1,480 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ar:
- discourse_automation:
- ai:
- flag_types:
- review: "إضافة المنشور إلى قائمة انتظار المراجعة"
- spam: "وضع علامة \"عشوائي\" على المنشور وإخفاؤه"
- spam_silence: "وضع علامة عشوائي على المنشور وإخفاؤه وكتم المستخدم"
- scriptables:
- llm_triage:
- title: فرز المنشورات باستخدام الذكاء الاصطناعي
- description: "فرز المنشورات باستخدام نموذج لغوي كبير"
- flagged_post: |
-
استجابة من النموذج:
-
%%LLM_RESPONSE%%
- تم تشغيله من قِبل %%AUTOMATION_NAME%% القاعدة.
- llm_report:
- title: التقرير الدوري باستخدام الذكاء الاصطناعي
- description: "تقرير دوري قائم على نموذج لغوي كبير"
- site_settings:
- discourse_ai_enabled: "تفعيل المكوِّن الإضافي Discourse AI."
- ai_toxicity_enabled: "تفعيل وحدة السلبية."
- ai_toxicity_inference_service_api_endpoint: "عنوان URL حيث تعمل واجهة API لوحدة السلبية"
- ai_toxicity_inference_service_api_key: "مفتاح API لواجهة API الخاصة بنموذج السلبية"
- ai_toxicity_inference_service_api_model: "النموذج المراد استخدامه في الاستدلال. نموذج متعدد اللغات يعمل مع اللغة الإيطالية والفرنسية والروسية والبرتغالية والإسبانية والتركية."
- ai_toxicity_flag_automatically: "الإبلاغ تلقائيًا عن المنشورات/رسائل الدردشة التي تتجاوز الحدود التي تم تكوينها."
- ai_toxicity_flag_threshold_toxicity: "السلبية: تعليق فظ أو غير محترم أو غير معقول، والذي من المحتمل بعض الشيء أن يجعلك تغادر مناقشة أو تتخلى عن مشاركة وجهة نظرك"
- ai_toxicity_flag_threshold_severe_toxicity: "السلبية الشديدة: تعليق شديد الكراهية أو هجومي أو غير محترم، والذي من المحتمل جدًا أن يجعلك تغادر مناقشة أو تتخلى عن مناقشة وجهة نظرك"
- ai_toxicity_flag_threshold_obscene: "فاحش"
- ai_toxicity_flag_threshold_identity_attack: "هجوم انتحال الشخصية"
- ai_toxicity_flag_threshold_insult: "إهانة"
- ai_toxicity_flag_threshold_threat: "تهديد"
- ai_toxicity_flag_threshold_sexual_explicit: "محتوى جنسي صريح"
- ai_toxicity_groups_bypass: "لن يتم تصنيف منشورات المستخدمين في هذه المجموعات حسب وحدة السلبية."
- ai_sentiment_enabled: "تفعيل وحدة المشاعر."
- ai_sentiment_inference_service_api_endpoint: "عنوان URL حيث تعمل واجهة API لوحدة المشاعر"
- ai_sentiment_inference_service_api_key: "مفتاح API لواجهة API الخاصة بنموذج المشاعر"
- ai_sentiment_models: "النماذج المراد استخدامها في الاستدلال. يصنِّف نموذج المشاعر المنشورات إلى إيجابية/محايدة/سلبية. ويصنِّفها نموذج العاطفة إلى غضب/اشمئزاز/خوف/فرح/حياد/حزن/مفاجأة."
- ai_nsfw_detection_enabled: "تفعيل وحدة NSFW (غير آمن لبيئة العمل)."
- ai_nsfw_inference_service_api_endpoint: "عنوان URL حيث تعمل واجهة API لوحدة NSFW (غير آمن لبيئة العمل)"
- ai_nsfw_inference_service_api_key: "مفتاح API لواجهة API الخاصة بنموذج NSFW (غير آمن لبيئة العمل)"
- ai_nsfw_flag_automatically: "الإبلاغ تلقائيًا عن المنشورات غير الآمنة للعمل التي تتجاوز الحدود التي تم تكوينها."
- ai_nsfw_flag_threshold_general: "الحد العام للصورة ليتم اعتبارها غير آمنة لبيئة العمل."
- ai_nsfw_flag_threshold_drawings: "الحد الأدنى للرسمة ليتم اعتبارها غير آمنة لبيئة العمل."
- ai_nsfw_flag_threshold_hentai: "الحد الأدنى للصورة المصنَّفة كمحتوى \"هنتاي\" ليتم اعتبارها غير آمنة لبيئة العمل."
- ai_nsfw_flag_threshold_porn: "الحد الأدنى للصورة المصنَّفة كمحتوى إباحي ليتم اعتبارها غير آمنة لبيئة العمل."
- ai_nsfw_flag_threshold_sexy: "الحد الأدنى للصورة المصنَّفة كمحتوى جنسي ليتم اعتبارها غير آمنة لبيئة العمل."
- ai_nsfw_models: "نماذج المراد استخدامها في الاستدلال على المحتوى غير الآمن لبيئة العمل."
- ai_helper_enabled: "تفعيل مساعد الذكاء الاصطناعي"
- composer_ai_helper_allowed_groups: "سيرى المستخدمون في هذه المجموعات زر مساعد الذكاء الاصطناعي في أداة الإنشاء."
- ai_helper_allowed_in_pm: "تفعيل مساعد الذكاء الاصطناعي في الرسائل الخاصة."
- ai_helper_model: "النموذج المراد استخدامه لمساعد الذكاء الاصطناعي."
- ai_helper_custom_prompts_allowed_groups: "سيرى المستخدمون في هذه المجموعات خيار رسالة المطالبة المخصَّصة في مساعد الذكاء الاصطناعي."
- ai_helper_automatic_chat_thread_title_delay: "التأخير بالدقائق قبل أن يقوم مساعد الذكاء الاصطناعي بتعيين عنوان سلسلة الدردشة تلقائيًا."
- ai_helper_automatic_chat_thread_title: "تعيين عناوين سلسلة الدردشة تلقائيًا بناءً على محتويات السلسلة."
- ai_helper_illustrate_post_model: "النموذج المراد استخدامه لميزة تزويد المنشور بالصور من مساعد الذكاء الاصطناعي في أداة الإنشاء"
- ai_helper_enabled_features: "حدِّد الميزات التي تريد تفعيلها في مساعد الذكاء الاصطناعي."
- post_ai_helper_allowed_groups: "مجموعات المستخدمين المسموح لها بالوصول إلى ميزات مساعد الذكاء الاصطناعي في المنشورات"
- ai_helper_image_caption_model: "حدِّد النموذج الذي سيتم استخدامه لإنشاء التسميات التوضيحية للصور"
- ai_auto_image_caption_allowed_groups: "يمكن للمستخدمين في هذه المجموعات تشغيل التسميات التوضيحية التلقائية للصور."
- ai_embeddings_selected_model: "استخدم النموذج المُحدَّد لتوليد التضمينات."
- ai_embeddings_generate_for_pms: "إنشاء تضمينات للرسائل الشخصية."
- ai_embeddings_semantic_related_topics_enabled: "استخدام البحث الدلالي للموضوعات ذات الصلة."
- ai_embeddings_semantic_related_topics: "أقصى عدد من الموضوعات لعرضها في قسم الموضوع ذي الصلة."
- ai_embeddings_backfill_batch_size: "عدد التضمينات المراد إعادة ملئها كل 15 دقيقة."
- ai_embeddings_semantic_search_enabled: "تفعيل البحث الدلالي في الصفحة كاملةً."
- ai_embeddings_semantic_quick_search_enabled: "تفعيل خيار البحث الدلالي في قائمة البحث المنبثقة."
- ai_embeddings_semantic_related_include_closed_topics: "تضمين الموضوعات المغلقة في نتائج البحث الدلالي"
- ai_embeddings_semantic_search_hyde_model: "النموذج المُستخدَم لتوسيع الكلمات الرئيسية للحصول على نتائج أفضل في أثناء البحث الدلالي"
- ai_embeddings_per_post_enabled: إنشاء التضمينات لكل منشور
- ai_summarization_model: "النموذج الذي سيتم استخدامه في التلخيص"
- ai_custom_summarization_allowed_groups: "المجموعات المسموح لها بإنشاء ملخصات جديدة."
- ai_pm_summarization_allowed_groups: "المجموعات المسموح لها بإنشاء الملخصات وعرضها في الرسائل الخاصة"
- ai_summary_gists_allowed_groups: "المجموعات المسموح لها برؤية ملخصات الموضوعات في قائمة الموضوعات الساخنة"
- ai_summary_backfill_maximum_topics_per_hour: "عدد ملخصات الموضوعات المطلوب ملؤها في الساعة."
- ai_bot_enabled: "تفعيل وحدة روبوت الذكاء الاصطناعي"
- ai_bot_enable_chat_warning: "عرض تحذير عند بدء الدردشة في رسالة خاصة. يمكن تجاوزه عن طريق تعديل سلسلة الترجمة: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "عندما يتمكن GPT Bot من الوصول إلى الرسالة الخاصة، فإنه سيرد على أعضاء هذه المجموعات."
- ai_bot_debugging_allowed_groups: "السماح لهذه المجموعات برؤية زر تصحيح الأخطاء في المنشورات التي تعرض طلب الذكاء الاصطناعي الأولي والرد"
- ai_bot_public_sharing_allowed_groups: "السماح لهذه المجموعات بمشاركة رسائل الذكاء الاصطناعي الشخصية مع العامة عبر رابط فريد متاح بشكلٍ علني. ملاحظة: إذا كان موقعك يتطلَّب تسجيل الدخول، فستتطلب المشاركة تسجيل الدخول أيضًا."
- ai_bot_add_to_header: "عرض زر في الرأس لبدء رسالة خاصة مع روبوت ذكاء اصطناعي"
- ai_bot_github_access_token: "رمز وصول GitHub للاستخدام مع أدوات الذكاء الاصطناعي من GitHub (مطلوب لدعم البحث)"
- ai_stability_api_key: "مفتاح API لواجهة API المسماة stability.ai"
- ai_stability_engine: "محرك إنشاء الصور المراد استخدامه لواجهة برمجة التطبيقات المسماة stability.ai"
- ai_stability_api_url: "عنوان URL لواجهة API المسماة stability.ai"
- ai_google_custom_search_api_key: "مفتاح API لواجهة API الخاصة ببحث Google المخصَّص، راجع: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "تجربة العملاء لواجهة بحث Google المخصَّصة"
- reviewables:
- reasons:
- flagged_by_toxicity: أبلغ المكوِّن الإضافي الذي يعمل بالذكاء الاصطناعي عن هذا بعد تصنيفه كسلبي.
- flagged_by_nsfw: أبلغ المكوِّن الإضافي الذي يعمل بالذكاء الاصطناعي عن هذا بعد تصنيفه كسلبي لصورة واحدة على الأقل من الصور الملحقة على أنها غير آمنة لبيئة العمل.
- reports:
- overall_sentiment:
- title: "المشاعر العامة"
- description: 'يقارن الرسم البياني عدد المنشورات المصنَّفة على أنها إما إيجابية أو سلبية. يتم حساب هذه النتائج عندما تكون الدرجات الإيجابية أو السلبية > درجة الحد المحدَّدة. وهذا يعني عدم عرض المنشورات المحايدة. يتم أيضًا استبعاد الرسائل الخاصة (PM). يتم تصنيفها باستخدام "cardiffnlp/twitter-roberta-base-sentiment-latest"'
- xaxis: "إيجابية (%)"
- yaxis: "التاريخ"
- emotion_admiration:
- title: "\U0001F929 الإعجاب"
- description: "المنشورات المصنَّفة برمز الإعجاب عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_amusement:
- title: "\U0001F604 التسلية"
- description: "المنشورات المصنَّفة برمز التسلية عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_anger:
- title: "\U0001F620 الغضب"
- description: "المنشورات المصنَّفة برمز الغضب عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_annoyance:
- title: "\U0001F612 الإزعاج"
- description: "المنشورات المصنَّفة برمز الإزعاج عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_approval:
- title: "\U0001F44D الموافقة"
- description: "المنشورات المصنَّفة برمز الموافقة عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_caring:
- title: "\U0001F917 الاهتمام"
- description: "المنشورات المصنَّفة برمز الاهتمام عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_confusion:
- title: "\U0001F615 الارتباك"
- description: "المنشورات المصنَّفة برمز الارتباك عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_curiosity:
- title: "\U0001F914 الفضول"
- description: "المنشورات المصنَّفة برمز الفضول عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_desire:
- title: "\U0001F60D الرغبة"
- description: "المنشورات المصنَّفة برمز الرغبة عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_disappointment:
- title: "\U0001F61E خيبة الأمل"
- description: "المنشورات المصنَّفة برمز خيبة الأمل عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_disapproval:
- title: "\U0001F44E عدم الموافقة"
- description: "المنشورات المصنَّفة برمز عدم الموافقة عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_disgust:
- title: "\U0001F922 الاشمئزاز"
- description: "المنشورات المصنَّفة برمز الاشمئزاز عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_embarrassment:
- title: "\U0001F633 الإحراج"
- description: "المنشورات المصنَّفة برمز الإحراج عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_excitement:
- title: "\U0001F92A الإثارة"
- description: "المنشورات المصنَّفة برمز الإثارة عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_fear:
- title: "\U0001F628 الخوف"
- description: "المنشورات المصنَّفة برمز الخوف عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_gratitude:
- title: "\U0001F64F الامتنان"
- description: "المنشورات المصنَّفة برمز الامتنان عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_grief:
- title: "\U0001F622 الكآبة"
- description: "المنشورات المصنَّفة برمز الكآبة عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_joy:
- title: "\U0001F60A المرح"
- description: "المنشورات المصنَّفة برمز الفرح عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_love:
- title: '❤️ الحب'
- description: "المنشورات المصنَّفة برمز الحب عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_nervousness:
- title: "\U0001F630 التوتر"
- description: "المنشورات المصنَّفة برمز التوتر عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_neutral:
- title: "\U0001F610 محايدة"
- description: "المنشورات المصنَّفة برمز الحياد عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_optimism:
- title: "\U0001F31F التفاؤل"
- description: "المنشورات المصنَّفة برمز التفاؤل عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_pride:
- title: "\U0001F981 الفخر"
- description: "المنشورات المصنَّفة برمز الفخر عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_realization:
- title: "\U0001F4A1 الإدراك"
- description: "المنشورات المصنَّفة برمز الإدراك عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_relief:
- title: "\U0001F60C الراحة"
- description: "المنشورات المصنَّفة برمز الراحة عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_remorse:
- title: "\U0001F614 الندم"
- description: "المنشورات المصنَّفة برمز الندم عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_sadness:
- title: "\U0001F62D الحزن"
- description: "المنشورات المصنَّفة برمز الحزن عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- emotion_surprise:
- title: "\U0001F632 المفاجأة"
- description: "المنشورات المصنَّفة برمز المفاجأة عن طريق الذكاء الاصطناعي، باستخدام نموذج \"Samlowe/roberta-base-go_emotions\""
- discourse_ai:
- ai_artifact:
- view_source: "عرض المصدر"
- view_changes: "عرض التغييرات"
- unknown_model: "نموذج ذكاء اصطناعي غير معروف"
- tools:
- custom_name: "%{name} (custom)"
- presets:
- browse_web_jina:
- name: "تصفح الويب (jina.ai)"
- exchange_rate:
- name: "سعر الصرف"
- stock_quote:
- name: "أسعار الأسهم (AlphaVantage)"
- image_generation:
- name: "مولِّد صور Flux (Together.ai)"
- empty_tool:
- name: "ابدأ من لا شيء..."
- ai_helper:
- errors:
- completion_request_failed: "حدث خطأ في أثناء محاولة تقديم اقتراحات. يُرجى إعادة المحاولة."
- prompts:
- translate: الترجمة إلى %{language}
- generate_titles: اقتراح عناوين للموضوعات
- proofread: تدقيق النص لغويًا
- markdown_table: إنشاء جدول Markdown
- custom_prompt: "رسالة مطالبة مخصَّصة"
- explain: "الشرح"
- illustrate_post: "تزويد المنشور بالصور"
- replace_dates: "التواريخ الذكية"
- painter:
- attribution:
- stable_diffusion_xl: "الصورة بواسطة Stable Diffusion XL"
- dall_e_3: "الصورة بواسطة DALL-E 3"
- image_caption:
- attribution: "تم إنشاء التسمية التوضيحية بالذكاء الاصطناعي"
- share_ai:
- read_more: "قراءة النص الكامل"
- onebox_title: "محادثة الذكاء الاصطناعي مع %{llm_name}"
- formatted_excerpt: "محادثة الذكاء الاصطناعي مع %{llm_name}:\n %{excerpt}"
- title: "%{title} - محادثة الذكاء الاصطناعي - %{site_name}"
- errors:
- not_allowed: "غير مسموح لك بمشاركة هذا الموضوع"
- other_people_in_pm: "لا يمكن مشاركة الرسائل الشخصية مع البشر الآخرين بشكلٍ علني"
- other_content_in_pm: "لا يمكن مشاركة الرسائل الشخصية التي تحتوي على منشورات من أشخاص آخرين بشكلٍ علني"
- failed_to_share: "فشلت مشاركة المحادثة"
- conversation_deleted: "تم حذف مشاركة المحادثة بنجاح"
- spam_detection:
- flag_reason: "تم الإبلاغ عنه كسلوكٍ عشوائي بواسطة Discourse AI"
- silence_reason: "تم إسكات المستخدم تلقائيًا بواسطة Discourse AI"
- invalid_error_type: "تم إدخال نوع خطأ غير صالح"
- unexpected: "حدث خطأ غير متوقع"
- bot_user_update_failed: "فشل تحديث مستخدم روبوت فحص السلوك العشوائي"
- ai_bot:
- reply_error: "عذراً، يبدو أن نظامنا واجه مشكلة غير متوقعة أثناء محاولة الرد.\n\n[details='Error details']\n%{details}\n[/details]"
- default_pm_prefix: "[رسالة خاصة دون عنوان من روبوت ذكاء اصطناعي]"
- personas:
- default_llm_required: "نموذج اللغة الكبير الافتراضي مطلوب قبل تفعيل الدردشة"
- cannot_delete_system_persona: "لا يمكن حذف شخصيات النظام، يُرجى إيقافها بدلًا من ذلك"
- cannot_edit_system_persona: "يمكن إعادة تسمية شخصيات النظام فقط، ولا يجوز لك تعديل الأدوات أو رسائل مطالبة النظام. قم بإيقافها وعمل نسخة منها بدلًا من ذلك."
- github_helper:
- name: "مساعد GitHub"
- description: "روبوت ذكاء اصطناعي متخصص في المساعدة في المهام والأسئلة المتعلقة بمنصة GitHub"
- general:
- name: مساعد المنتدى
- description: "روبوت ذكاء اصطناعي للأغراض العامة قادر على أداء مهام مختلفة"
- artist:
- name: فنان
- description: "روبوت ذكاء اصطناعي متخصص في إنشاء الصور"
- sql_helper:
- name: مساعد SQL
- description: "روبوت ذكاء اصطناعي متخصص في المساعدة في صياغة استعلامات SQL في مثيل Discourse هذا"
- settings_explorer:
- name: مستكشف الإعدادات
- description: "روبوت ذكاء اصطناعي متخصص في المساعدة في استكشاف إعدادات موقع Discourse"
- creative:
- name: المبدع
- description: "روبوت ذكاء اصطناعي من دون تكاملات خارجية متخصصة في المهام الإبداعية"
- dall_e3:
- name: "DALL-E 3"
- description: "روبوت ذكاء اصطناعي متخصص في إنشاء الصور باستخدام DALL-E 3"
- discourse_helper:
- name: "مساعد Discourse"
- description: "روبوت ذكاء اصطناعي متخصص في المساعدة في المهام المتعلقة بمنصة Discourse"
- web_artifact_creator:
- name: "مُنشئ الملفات الثانوية على الويب"
- description: "روبوت الذكاء الاصطناعي المتخصص في إنشاء ملفات ثانوية تفاعلية على الويب"
- custom_prompt:
- name: "رسالة مطالبة مخصَّصة"
- smart_dates:
- name: "التواريخ الذكية"
- topic_not_found: "الملخص غير متوفر، الموضوع غير موجود!"
- summarizing: "جارٍ تلخيص الموضوع"
- searching: "جارٍ البحث عن: '%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "الحد الأقصى لعدد النتائج"
- google:
- base_query:
- name: "استعلام البحث الأساسي"
- description: "الاستعلام الأساسي الذي سيتم استخدامه عند البحث. الأمثلة: سيتضمَّن \"site:example.com\" النتائج من example.com فقط، بينما سيتضمَّن before:2022-01-01 النتائج من عام 2021 وما قبله فقط. تتم إضافة هذا النص إلى استعلام البحث."
- read:
- read_private:
- name: "قراءة خاصة"
- description: "السماح بالوصول إلى جميع الموضوعات التي يمكن للمستخدم الوصول إليها (يتم تضمين الموضوعات العامة فقط بشكلٍ افتراضي)"
- search:
- search_private:
- name: "بحث خاص"
- description: "تضمين جميع الموضوعات التي يمكن للمستخدم الوصول إليها في نتائج البحث (يتم تضمين الموضوعات العامة فقط بشكلٍ افتراضي)"
- max_results:
- name: "الحد الأقصى لعدد النتائج"
- description: "الحد الأقصى لعدد النتائج المطلوب تضمينها في البحث - في حال ترك الحقل فارغًا، سيتم استخدام القواعد الافتراضية وستتم زيادة العدد وفقًا للنموذج المُستخدَم. أعلى قيمة هي 100."
- base_query:
- name: "استعلام البحث الأساسي"
- description: "استعلام البحث الأساسي الذي سيتم استخدامه عند البحث. على سبيل المثال: سيسبق \"#urgent\" \"#urgent\" في استعلام البحث وسيتضمن الموضوعات ذات الفئة أو الوسم العاجل فقط."
- tool_summary:
- update_artifact: "تحديث ملف ثانوي على الويب"
- create_artifact: "إنشاء ملف ثانوي على الويب"
- web_browser: "تصفح الويب"
- github_search_files: "ملفات بحث GitHub"
- github_search_code: "البحث عن رمز برمجي على GitHub"
- github_file_content: "محتوى ملف GitHub"
- github_pull_request_diff: "الفروقات في طلب السحب على GitHub"
- random_picker: "منتقي عشوائي"
- categories: "إدراج الفئات"
- search: "البحث"
- tags: "إدراج الوسوم"
- time: "الوقت"
- summarize: "تلخيص"
- image: "إنشاء صورة"
- google: "البحث في Google"
- read: "قراءة الموضوع"
- setting_context: "البحث عن سياق إعداد الموقع"
- schema: "البحث عن مخطط قاعدة البيانات"
- search_settings: "جارٍ البحث في إعدادات الموقع"
- dall_e: "إنشاء صورة"
- search_meta_discourse: "البحث في Meta Discourse"
- javascript_evaluator: "تقييم JavaScript"
- tool_help:
- update_artifact: "تحديث ملف ثانوي على الويب باستخدام روبوت الذكاء الاصطناعي"
- create_artifact: "إنشاء ملف ثانوي على الويب باستخدام روبوت الذكاء الاصطناعي"
- web_browser: "تصفح صفحة الويب باستخدام روبوت ذكاء اصطناعي"
- github_search_code: "البحث عن رمز برمجي في مستودع GitHub"
- github_search_files: "البحث عن ملفات في مستودع GitHub"
- github_file_content: "استعادة محتوى ملفات من مستودع GitHub"
- github_pull_request_diff: "استعادة الفروقات في طلب السحب على GitHub"
- random_picker: "اختر رقمًا عشوائيًا أو عنصرًا عشوائيًا من القائمة"
- categories: "إدراج جميع الفئات المرئية بشكلٍ عام في المنتدى"
- search: "البحث في جميع الموضوعات العامة في المنتدى"
- tags: "إدراج جميع الوسوم في المنتدى"
- time: "البحث عن الوقت في مناطق زمنية مختلفة"
- summary: "تلخيص موضوع"
- image: "إنشاء صورة باستخدام Stable Diffusion"
- google: "البحث في Google عن استعلام"
- read: "قراءة الموضوع العام في المنتدى"
- setting_context: "البحث عن سياق إعداد الموقع"
- schema: "البحث عن مخطط قاعدة البيانات"
- search_settings: "البحث في إعدادات الموقع"
- dall_e: "إنشاء صورة باستخدام DALL-E 3"
- search_meta_discourse: "البحث في Meta Discourse"
- javascript_evaluator: "تقييم JavaScript"
- tool_description:
- update_artifact: "تم تحديث ملف ثانوي على الويب باستخدام روبوت الذكاء الاصطناعي"
- web_browser: "قراءة %{url}"
- github_search_files: "تم البحث عن '%{keywords}' في %{repo}/%{branch}"
- github_search_code: "تم البحث عن '%{query}' في %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "تم استعادة محتوى %{file_paths} من %{repo_name}@%{branch}"
- random_picker: "الاختيار من %{options}، تم اختيار: %{result}"
- read: "القراءة: %{title}"
- time: "الوقت في المنطقة الزمنية %{timezone} هو %{time}"
- summarize: "تم تلخيص %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- zero: "تم العثور على %{count} فئات"
- one: "تم العثور على فئة واحدة (%{count})"
- two: "تم العثور على فئتين (%{count})"
- few: "تم العثور على %{count} فئات"
- many: "تم العثور على %{count} فئات"
- other: "تم العثور على %{count} فئات"
- tags:
- zero: "تم العثور على %{count} وسمًا"
- one: "تم العثور على وسم واحد (%{count})"
- two: "تم العثور على وسمين (%{count})"
- few: "تم العثور على %{count} وسوم"
- many: "تم العثور على %{count} وسمًا"
- other: "تم العثور على %{count} وسمًا"
- search:
- zero: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- one: "تم العثور على %{count} نتيجة لاستعلام البحث '%{query}'"
- two: "تم العثور على نتيجتين (%{count}) لاستعلام البحث '%{query}'"
- few: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- many: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- other: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- search_meta_discourse:
- zero: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- one: "تم العثور على %{count} نتيجة لاستعلام البحث '%{query}'"
- two: "تم العثور على نتيجتين (%{count}) لاستعلام البحث '%{query}'"
- few: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- many: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- other: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- google:
- zero: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- one: "تم العثور على %{count} نتيجة لاستعلام البحث '%{query}'"
- two: "تم العثور على نتيجتين (%{count}) لاستعلام البحث '%{query}'"
- few: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- many: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- other: "تم العثور على %{count} نتائج لاستعلام البحث '%{query}'"
- setting_context: "سياق القراءة لـ: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- zero: "تم العثور على %{count} نتائج لـ '%{query}'"
- one: "تم العثور على نتيجة واحدة (%{count}) لـ '%{query}'"
- two: "تم العثور على نتيجتين (%{count}) لـ '%{query}'"
- few: "تم العثور على %{count} نتائج لـ '%{query}'"
- many: "تم العثور على %{count} نتائج لـ '%{query}'"
- other: "تم العثور على %{count} نتائج لـ '%{query}'"
- summarization:
- configuration_hint:
- zero: "قم بتكوين هذه الإعدادات أولًا: %{settings}"
- one: "قم بتكوين الإعداد `%{setting}` أولًا."
- two: "قم بتكوين هذين الإعدادين أولًا: %{settings}"
- few: "قم بتكوين هذه الإعدادات أولًا: %{settings}"
- many: "قم بتكوين هذه الإعدادات أولًا: %{settings}"
- other: "قم بتكوين هذه الإعدادات أولًا: %{settings}"
- chat:
- no_targets: "لم تكن هناك رسائل خلال الفترة المحدَّدة."
- sentiment:
- reports:
- overall_sentiment: "المشاعر العامة (إيجابية - سلبية)"
- post_emotion:
- sadness: "الحزن \U0001F622"
- surprise: "المفاجأة \U0001F631"
- neutral: "محايدة \U0001F610"
- fear: "الخوف \U0001F628"
- anger: "الغضب \U0001F621"
- joy: "المرح \U0001F600"
- disgust: "الاشمئزاز \U0001F922"
- sentiment_analysis:
- positive: "إيجابية"
- negative: "سلبية"
- neutral: "محايدة"
- llm:
- configuration:
- disable_module_first: "يجب عليك إيقاف %{setting} أولًا."
- set_llm_first: "اضبط %{setting} أولًا"
- model_unreachable: "لم نتمكن من الحصول على رد من هذا النموذج. تحقَّق من إعداداتك أولًا."
- invalid_seeded_model: "لا يمكنك استخدام هذا النموذج مع هذه الميزة"
- must_select_model: "يجب عليك تحديد نموذج لغة كبير أولًا"
- endpoints:
- not_configured: "%{display_name} (not configured)"
- configuration_hint:
- zero: "تأكَّد من تكوين الإعداد `%{settings}`."
- one: "تأكَّد من تكوين الإعداد `%{settings}`."
- two: "تأكَّد من تكوين الإعدادَين `%{settings}`."
- few: "تأكَّد من تكوين الإعدادات `%{settings}`."
- many: "تأكَّد من تكوين الإعدادات `%{settings}`."
- other: "تأكَّد من تكوين الإعدادات `%{settings}`."
- delete_failed:
- zero: "لم نتمكن من حذف هذا النموذج لأن %{settings} يستخدمه. قم بتحديث الإعداد وحاول مرة أخرى."
- one: "لم نتمكن من حذف هذا النموذج لأن %{settings} يستخدمه. قم بتحديث الإعداد وحاول مرة أخرى."
- two: "لم نتمكن من حذف هذا النموذج لأن %{settings} يستخدمانه. قم بتحديث الإعدادَين وحاول مرة أخرى."
- few: "لم نتمكن من حذف هذا النموذج لأن %{settings} تستخدمه. قم بتحديث الإعدادات وحاول مرة أخرى."
- many: "لم نتمكن من حذف هذا النموذج لأن %{settings} تستخدمه. قم بتحديث الإعدادات وحاول مرة أخرى."
- other: "لم نتمكن من حذف هذا النموذج لأن %{settings} تستخدمه. قم بتحديث الإعدادات وحاول مرة أخرى."
- cannot_edit_builtin: "لا يمكنك تعديل نموذج مُدمَج."
- embeddings:
- delete_failed: "هذا النموذج قيد الاستخدام حاليًا. قم بتحديث `ai embeddings selected model` أولًا."
- cannot_edit_builtin: "لا يمكنك تعديل نموذج مُدمَج."
- configuration:
- disable_embeddings: "يجب عليك إيقاف 'ai embeddings enabled' أولًا."
- choose_model: "قم بتعيين 'ai embeddings selected model' أولًا."
- llm_models:
- missing_provider_param: "لا يمكن ترك %{param} فارغة"
- bedrock_invalid_url: "يُرجى ملء جميع الحقول لاستخدام هذا النموذج."
- ai_staff_action_logger:
- updated: "تاريخ التحديث"
- removed: "تمت إزالتها"
- errors:
- quota_exceeded: "لقد تجاوزت الحصة المُخصَّصة لهذا النموذج. يُرجى إعادة المحاولة بعد %{relative_time}."
- quota_required: "يجب عليك تحديد الحد الأقصى للرموز أو الاستخدامات لهذا النموذج"
- no_query_specified: معلمة الاستعلام مطلوبة، يُرجى تحديدها.
- no_user_for_persona: لا تملك الشخصية المحدَّدة مستخدمًا مرتبطًا بها.
- persona_not_found: الشخصية المحدَّدة غير موجودة. تحقَّق من معلمتَي persona_name أو persona_id.
- no_user_specified: اسم المستخدم أو معلمة user_unique_id مطلوبة، يُرجى تحديدها.
- user_not_found: المستخدم المحدَّد غير موجود. تحقَّق من معلمة username.
- persona_disabled: الشخصية المحدَّدة متوقفة. تحقَّق من معلمتَي persona_name أو persona_id.
- no_default_llm: يجب أن يكون للشخصية معلمة default_llm محدَّدة.
- user_not_allowed: غير مسموح للمستخدم بالمشاركة في الموضوع.
- prompt_message_length: تتجاوز الرسالة الحد الأقصى لعدد الحروف، والبالغ 1000 حرف، بمقدار %{idx}.
diff --git a/config/locales/server.be.yml b/config/locales/server.be.yml
deleted file mode 100644
index 37264e26..00000000
--- a/config/locales/server.be.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-be:
- reports:
- emotion_neutral:
- title: "\U0001F610 нейтральны"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Пошук"
- sentiment:
- reports:
- post_emotion:
- neutral: "нейтральны \U0001F610"
- sentiment_analysis:
- neutral: "нейтральны"
diff --git a/config/locales/server.bg.yml b/config/locales/server.bg.yml
deleted file mode 100644
index d8266523..00000000
--- a/config/locales/server.bg.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-bg:
- reports:
- overall_sentiment:
- yaxis: "Дата"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Търсене"
- time: "Време "
- summarize: "Обобщаване"
- ai_staff_action_logger:
- removed: "премахнато"
diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml
deleted file mode 100644
index 1e36c099..00000000
--- a/config/locales/server.bs_BA.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-bs_BA:
- reports:
- overall_sentiment:
- yaxis: "Datum"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Pretraži"
- time: "Vrijeme"
- ai_staff_action_logger:
- updated: "ažurirano"
diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml
deleted file mode 100644
index 9f29be30..00000000
--- a/config/locales/server.ca.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ca:
- reports:
- overall_sentiment:
- yaxis: "Data"
- emotion_neutral:
- title: "\U0001F610 Neutre"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Cerca"
- time: "Hora"
- sentiment:
- reports:
- post_emotion:
- neutral: "Neutre \U0001F610"
- sentiment_analysis:
- neutral: "Neutre"
- ai_staff_action_logger:
- updated: "actualitzat"
diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml
deleted file mode 100644
index 94f870c1..00000000
--- a/config/locales/server.cs.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-cs:
- reports:
- overall_sentiment:
- yaxis: "Datum"
- discourse_ai:
- ai_helper:
- prompts:
- translate: Přeložit do %{language}
- generate_titles: Navrhnout názvy témat
- proofread: Korektura textu
- markdown_table: Generovat Markdown tabulku
- custom_prompt: "Vlastní pokyn"
- explain: "Vysvětlit"
- illustrate_post: "Ilustrovat příspěvek"
- replace_dates: "Chytré datumy"
- ai_bot:
- personas:
- custom_prompt:
- name: "Vlastní pokyn"
- smart_dates:
- name: "Chytré datumy"
- topic_not_found: "Souhrn není k dispozici, téma nebylo nalezeno!"
- summarizing: "Vytvářím souhrn tématu"
- tool_summary:
- search: "Vyhledat"
- tags: "Seznam značek"
- time: "Čas"
- tool_help:
- summary: "Shrnout téma"
- ai_staff_action_logger:
- updated: "aktualizováno"
- removed: "odstraněno"
diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml
deleted file mode 100644
index 32d16ea1..00000000
--- a/config/locales/server.da.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-da:
- reports:
- overall_sentiment:
- yaxis: "Dato"
- emotion_neutral:
- title: "\U0001F610 Neutral"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Søg"
- time: "Tidspunkt"
- sentiment:
- reports:
- post_emotion:
- neutral: "Neutral \U0001F610"
- sentiment_analysis:
- neutral: "Neutral"
- ai_staff_action_logger:
- updated: "opdateret"
- removed: "fjernet"
diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml
deleted file mode 100644
index 35b10698..00000000
--- a/config/locales/server.de.yml
+++ /dev/null
@@ -1,614 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-de:
- discourse_automation:
- ai:
- flag_types:
- review: "Beitrag zur Überprüfungswarteschlange hinzufügen"
- review_hide: "Beitrag zur Warteschlange zur Überprüfung hinzufügen und Beitrag verstecken"
- spam: "Als Spam markieren und Beitrag verbergen"
- spam_silence: "Als Spam melden, Beitrag ausblenden und Benutzer stummschalten"
- scriptables:
- llm_tool_triage:
- title: Sichtung von Beiträgen mit dem KI-Tool
- description: "Sichtung von Beiträgen mit benutzerdefinierter Logik in einem KI-Tool"
- llm_persona_triage:
- title: Sichtung von Beiträgen mit KI-Persona
- description: "Reagiere auf Beiträge mit einer bestimmten KI-Persona"
- llm_triage:
- title: Beiträge mithilfe von KI sortieren
- description: "Beiträge mithilfe eines großen Sprachmodells sortieren"
- flagged_post: |
-
Antwort des Modells:
-
%%LLM_RESPONSE%%
- Ausgelöst durch die Regel %%AUTOMATION_NAME%%.
- llm_report:
- title: Regelmäßiger Bericht mit KI
- description: "Regelmäßiger Bericht auf der Grundlage eines großen Sprachmodells"
- site_settings:
- discourse_ai_enabled: "Aktiviere das Discourse-KI-Plug-in."
- ai_artifact_security: "Das KI-Artefaktsystem generiert IFRAMEs mit ausführbarem Code. Der strikte Modus erfordert einen zusätzlichen Klick zum Ausführen des Codes. Der laxe Modus führt Code sofort aus. Im Hybridmodus kann der Benutzer data-ai-artifact-autorun angeben, um den Code sofort anzuzeigen. Der deaktivierte Modus deaktiviert das Artefaktsystem."
- ai_toxicity_enabled: "Aktiviere das Toxizitätsmodul."
- ai_toxicity_inference_service_api_endpoint: "URL, unter der die API für das Toxizitätsmodul läuft"
- ai_toxicity_inference_service_api_key: "API-Schlüssel für die Toxizitäts-API"
- ai_toxicity_inference_service_api_model: "Modell, das für die Inferenz verwendet wird. Das mehrsprachige Modell funktioniert mit Italienisch, Französisch, Russisch, Portugiesisch, Spanisch und Türkisch."
- ai_toxicity_flag_automatically: "Markiere automatisch Beiträge/Chat-Nachrichten, die über den konfigurierten Schwellenwerten liegen."
- ai_toxicity_flag_threshold_toxicity: "Toxizität: ein unhöflicher, respektloser oder unangemessener Kommentar, der dich mit einiger Wahrscheinlichkeit dazu bringt, eine Diskussion zu verlassen oder deinen Standpunkt nicht mehr zu teilen"
- ai_toxicity_flag_threshold_severe_toxicity: "Drastische Toxizität: ein sehr hasserfüllter, aggressiver oder respektloser Kommentar, der dich mit hoher Wahrscheinlichkeit dazu bringt, eine Diskussion zu verlassen oder deinen Standpunkt nicht mehr zu teilen"
- ai_toxicity_flag_threshold_obscene: "Obszön"
- ai_toxicity_flag_threshold_identity_attack: "Identitätsangriff"
- ai_toxicity_flag_threshold_insult: "Beleidigung"
- ai_toxicity_flag_threshold_threat: "Bedrohung"
- ai_toxicity_flag_threshold_sexual_explicit: "Sexuell explizit"
- ai_toxicity_groups_bypass: "Die Beiträge von Benutzern in diesen Gruppen werden nicht durch das Toxizitätsmodul eingestuft."
- ai_sentiment_enabled: "Aktiviere das Stimmungsmodul."
- ai_sentiment_inference_service_api_endpoint: "URL, unter der die API für das Stimmungsmodul läuft"
- ai_sentiment_inference_service_api_key: "API-Schlüssel für die Stimmungsmodul-API"
- ai_sentiment_models: "Modelle, die für die Inferenz verwendet werden. Das Stimmungsmodul klassifiziert Beiträge in den Bereichen positiv/neutral/negativ. Emotionen werden in die Bereiche Wut/Ekel/Furcht/Freude/neutral/Traurigkeit/Überraschung eingeordnet."
- ai_nsfw_detection_enabled: "Aktiviere das NSFW-Modul."
- ai_nsfw_inference_service_api_endpoint: "URL, unter der die API für das NSFW-Modul läuft"
- ai_nsfw_inference_service_api_key: "API-Schlüssel für die NSFW-API"
- ai_nsfw_flag_automatically: "Kennzeichne automatisch NSFW-Beiträge, die über den konfigurierten Schwellenwerten liegen."
- ai_nsfw_flag_threshold_general: "Allgemeiner Schwellenwert, ab dem ein Bild als NSFW gilt."
- ai_nsfw_flag_threshold_drawings: "Schwellenwert, ab dem eine Zeichnung als NSFW gilt."
- ai_nsfw_flag_threshold_hentai: "Schwellenwert, ab dem ein Bild, das als Hentai eingestuft wird, als NSFW gilt."
- ai_nsfw_flag_threshold_porn: "Schwellenwert, ab dem ein Bild, das als Porno eingestuft wird, als NSFW gilt."
- ai_nsfw_flag_threshold_sexy: "Schwellenwert, ab dem ein Bild, das als sexy eingestuft wird, als NSFW gilt."
- ai_nsfw_models: "Modelle, die für NSFW-Inferenz verwendet werden."
- ai_spam_detection_enabled: "Aktiviere das KI-Spamerkennungsmodul"
- ai_openai_api_key: "API-Schlüssel für OpenAI-API. Wird NUR für Bilderstellung und -bearbeitung verwendet. Für GPT verwende die Registerkarte für die LLM-Konfiguration"
- ai_openai_image_generation_url: "URL für die OpenAI-Bilderstellungs-API"
- ai_openai_image_edit_url: "URL für die OpenAI-Bildbearbeitungs-API"
- ai_helper_enabled: "Aktiviere den KI-Helfer."
- composer_ai_helper_allowed_groups: "Benutzer dieser Gruppen sehen die KI-Helfer-Schaltfläche im Composer."
- ai_helper_allowed_in_pm: "Aktiviere den Composer-KI-Helfer in PN."
- ai_helper_model: "Modell, das für den KI-Helfer verwendet werden soll."
- ai_helper_custom_prompts_allowed_groups: "Die Benutzer dieser Gruppen sehen die Option „Benutzerdefinierte Eingabeaufforderung“ im KI-Helfer."
- ai_helper_automatic_chat_thread_title_delay: "Verzögerung in Minuten, bevor der KI-Helfer automatisch den Titel des Chat-Threads festlegt."
- ai_helper_automatic_chat_thread_title: "Lege die Titel der Chat-Threads automatisch anhand der Thread-Inhalte fest."
- ai_helper_illustrate_post_model: "Modell, das für die Funktion „Beitrag illustrieren“ des Composer-KI-Helfers verwendet wird"
- ai_helper_enabled_features: "Wähle die Funktionen aus, die im KI-Helfer aktiviert werden sollen."
- post_ai_helper_allowed_groups: "Nutzergruppen, die auf KI-Helfer-Funktionen in Beiträgen zugreifen dürfen"
- ai_helper_image_caption_model: "Wähle das Modell aus, das für die Erstellung von Bildbeschriftungen verwendet werden soll"
- ai_auto_image_caption_allowed_groups: "Benutzer dieser Gruppen können die automatische Bildbeschriftung ein- und ausschalten."
- ai_embeddings_selected_model: "Verwende das ausgewählte Modell für die Erzeugung von Einbettungen."
- ai_embeddings_generate_for_pms: "Erstelle Einbettungen für persönliche Nachrichten."
- ai_embeddings_semantic_related_topics_enabled: "Verwende die semantische Suche für verwandte Themen."
- ai_embeddings_semantic_related_topics: "Maximale Anzahl der Themen, die im Abschnitt für verwandte Themen angezeigt werden sollen."
- ai_embeddings_backfill_batch_size: "Anzahl der Einbettungen, die alle 15 Minuten aufgefüllt werden."
- ai_embeddings_semantic_search_enabled: "Aktiviere die semantische Ganzseitensuche."
- ai_embeddings_semantic_quick_search_enabled: "Aktiviere die semantische Suchoption im Such-Menü-Pop-up."
- ai_embeddings_semantic_related_include_closed_topics: "Geschlossene Themen in semantische Suchergebnisse einbeziehen"
- ai_embeddings_semantic_search_hyde_model: "Modell zur Erweiterung von Schlüsselwörtern, um bessere Ergebnisse bei der semantischen Suche zu erzielen"
- ai_embeddings_per_post_enabled: Erstelle Einbettungen für jeden Beitrag
- ai_summarization_enabled: "Aktiviere die Zusammenfassungsfunktion"
- ai_summarization_model: "Modell, das für die Zusammenfassung verwendet werden soll"
- ai_summarization_persona: "Persona, die für die Zusammenfassungsfunktion verwendet werden soll"
- ai_custom_summarization_allowed_groups: "Gruppen, die neue Zusammenfassungen erstellen dürfen."
- ai_pm_summarization_allowed_groups: "Gruppen, die Zusammenfassungen in PN erstellen und ansehen dürfen."
- ai_summary_gists_enabled: "Erstelle automatisch kurze Zusammenfassungen der letzten Antworten in Themen"
- ai_summary_gists_allowed_groups: "Gruppen, die zentrale Punkte in der Liste der aktuellen Themen sehen dürfen."
- ai_summary_backfill_maximum_topics_per_hour: "Anzahl der Themenzusammenfassungen, die pro Stunde nachgeholt werden."
- ai_bot_enabled: "Aktiviere das KI-Bot-Modul."
- ai_bot_enable_chat_warning: "Zeigt eine Warnung an, wenn der PN-Chat initiiert wird. Kann durch Bearbeiten der Übersetzungszeichenfolge überschrieben werden: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Wenn der GPT-Bot Zugriff auf die PN hat, wird er den Mitgliedern dieser Gruppen antworten."
- ai_bot_debugging_allowed_groups: "Erlaube diesen Gruppen, eine Debug-Schaltfläche in Beiträgen zu sehen, der die rohe KI-Anfrage und -Antwort anzeigt."
- ai_bot_public_sharing_allowed_groups: "Erlaube diesen Gruppen, persönliche KI-Nachrichten über einen eindeutigen, öffentlich zugänglichen Link mit der Öffentlichkeit zu teilen. Hinweis: Wenn für deine Website eine Anmeldung erforderlich ist, ist auch zum Ansehen des geteilten Inhalts eine Anmeldung erforderlich."
- ai_bot_add_to_header: "Eine Schaltfläche in der Kopfzeile anzeigen, um eine PN mit einem KI-Bot zu starten"
- ai_bot_github_access_token: "GitHub-Zugangstoken für die Verwendung mit den GitHub-KI-Tools (erforderlich für die Suchunterstützung)"
- ai_stability_api_key: "API-Schlüssel für die stability.ai-API"
- ai_stability_engine: "Bildgenerierungsengine für die stability.ai-API"
- ai_stability_api_url: "URL für die stability.ai-API"
- ai_google_custom_search_api_key: "API-Schlüssel für die Google Custom Search API, siehe: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX für Google Custom Search API"
- ai_discord_search_enabled: "Aktiviert die Discord-Suchfunktion"
- ai_discord_app_id: "Die ID der Discord-Anwendung, mit der du die Discord-Suche verbinden möchtest"
- ai_discord_app_public_key: "Der öffentliche Schlüssel der Discord-Anwendung, mit der du die Discord-Suche verbinden möchtest"
- ai_discord_search_mode: "Wähle den Suchmodus für die Discord-Suche aus"
- ai_discord_search_persona: "Die Persona, die für die Discord-Suche verwendet wird."
- ai_discord_allowed_guilds: "Discord-Gilden (Server), auf denen der Bot suchen darf"
- ai_bot_enable_dedicated_ux: "Erlaube eine Vollbild-Bot-Oberfläche anstelle einer PM"
- ai_translation_enabled: "Aktiviert die KI-Übersetzungsfunktion"
- ai_translation_model: "Das Modell, das für die Übersetzung verwendet werden soll. Dieses Modell muss die Übersetzung unterstützen. Personas können diese Einstellung überschreiben."
- ai_translation_backfill_limit_to_public_content: "Wenn diese Option aktiviert ist, werden nur Inhalte in öffentlichen Kategorien übersetzt. Wenn sie deaktiviert ist, werden auch Inhalte in Gruppen-PMs und privaten Kategorien zur Übersetzung gesendet."
- ai_translation_max_post_length: "Die maximale Länge eines zu übersetzenden Beitrags. Längere Beiträge werden nicht übersetzt."
- ai_translation_backfill_max_age_days: "Das maximale Alter eines Beitrags oder Themas, das übersetzt werden soll. Beiträge und Themen, die älter sind, werden nicht übersetzt. 0 deaktiviert die rückwirkende Übersetzung, aber nicht die Übersetzung von neuen Beiträgen."
- reviewables:
- reasons:
- flagged_by_toxicity: Das KI-Plug-in meldete dies nach der Klassifizierung als toxisch.
- flagged_by_nsfw: Das KI-Plug-in meldete dies nach der Klassifizierung von mindestens einem der angehängten Bilder als NSFW.
- reports:
- sentiment_analysis:
- title: "Stimmungsanalyse"
- description: "Dieser Bericht enthält eine Stimmungsanalyse für Beiträge, gruppiert nach Kategorie, mit positiven, negativen und neutralen Bewertungen für jeden Beitrag und jede Kategorie."
- overall_sentiment:
- title: "Allgemeine Stimmung"
- description: 'Das Diagramm vergleicht die Anzahl der Beiträge, die entweder als positiv oder negativ eingestuft werden. Sie werden berechnet, wenn die positive oder negative Bewertung über dem festgelegten Schwellenwert liegt. Das bedeutet, dass neutrale Beiträge nicht angezeigt werden. Persönliche Nachrichten (PN) sind ebenfalls ausgeschlossen. Klassifiziert mit „cardiffnlp/twitter-roberta-base-sentiment-latest“'
- xaxis: "Positiv (%)"
- yaxis: "Datum"
- emotion_admiration:
- title: "\U0001F929 Bewunderung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Bewunderung“ klassifiziert wurden."
- emotion_amusement:
- title: "\U0001F604 Unterhaltung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Unterhaltung“ klassifiziert wurden."
- emotion_anger:
- title: "\U0001F620 Wut"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Wut“ klassifiziert wurden."
- emotion_annoyance:
- title: "\U0001F612 Ärgernis"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Ärgernis“ klassifiziert wurden."
- emotion_approval:
- title: "\U0001F44D Zustimmung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Zustimmung“ klassifiziert wurden."
- emotion_caring:
- title: "\U0001F917 Fürsorge"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Fürsorge“ klassifiziert wurden."
- emotion_confusion:
- title: "\U0001F615 Verwirrung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Verwirrung“ klassifiziert wurden."
- emotion_curiosity:
- title: "\U0001F914 Neugierde"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Neugierde“ klassifiziert wurden."
- emotion_desire:
- title: "\U0001F60D Wunsch"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Wunsch“ klassifiziert wurden."
- emotion_disappointment:
- title: "\U0001F61E Enttäuschung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Enttäuschung“ klassifiziert wurden."
- emotion_disapproval:
- title: "\U0001F44E Missbilligung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Missbilligung“ klassifiziert wurden."
- emotion_disgust:
- title: "\U0001F922 Ekel"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Ekel“ klassifiziert wurden."
- emotion_embarrassment:
- title: "\U0001F633 Verlegenheit"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Verlegenheit“ klassifiziert wurden."
- emotion_excitement:
- title: "\U0001F92A Aufregung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Aufregung“ klassifiziert wurden."
- emotion_fear:
- title: "\U0001F628 Furcht"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Furcht“ klassifiziert wurden."
- emotion_gratitude:
- title: "\U0001F64F Dankbarkeit"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Dankbarkeit“ klassifiziert wurden."
- emotion_grief:
- title: "\U0001F622 Kummer"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Kummer“ klassifiziert wurden."
- emotion_joy:
- title: "\U0001F60A Freude"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Freude“ klassifiziert wurden."
- emotion_love:
- title: '❤️ Liebe'
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Liebe“ klassifiziert wurden."
- emotion_nervousness:
- title: "\U0001F630 Nervosität"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Nervosität“ klassifiziert wurden."
- emotion_neutral:
- title: "\U0001F610 Neutral"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Neutral“ klassifiziert wurden."
- emotion_optimism:
- title: "\U0001F31F Optimismus"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Optimismus“ klassifiziert wurden."
- emotion_pride:
- title: "\U0001F981 Stolz"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Stolz“ klassifiziert wurden."
- emotion_realization:
- title: "\U0001F4A1 Realisierung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Realisierung“ klassifiziert wurden."
- emotion_relief:
- title: "\U0001F60C Erleichterung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Erleichterung“ klassifiziert wurden."
- emotion_remorse:
- title: "\U0001F614 Reue"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Reue“ klassifiziert wurden."
- emotion_sadness:
- title: "\U0001F62D Traurigkeit"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Traurigkeit“ klassifiziert wurden."
- emotion_surprise:
- title: "\U0001F632 Überraschung"
- description: "Beiträge, die per KI unter Verwendung des Modells „SamLowe/roberta-base-go_emotions“ mit der Emotion „Überraschung“ klassifiziert wurden."
- discourse_ai:
- ai_artifact:
- errors:
- max_keys_exceeded:
- one: "Du kannst nur %{count} Schlüssel im Artefakt haben."
- other: "Du kannst nur %{count} Schlüssel im Artefakt haben."
- link: "In neuem Tab anzeigen"
- copy_embed: "Einbettung kopieren"
- view_source: "Quelle anzeigen"
- view_changes: "Änderungen anzeigen"
- change_description: "Beschreibung ändern"
- copied: "Wurde in Zwischenablage kopiert"
- unknown_model: "Unbekanntes KI-Modell"
- tools:
- custom_name: "%{name} (benutzerdefiniert)"
- presets:
- browse_web_jina:
- name: "Web durchsuchen (jina.ai)"
- exchange_rate:
- name: "Wechselkurs"
- stock_quote:
- name: "Aktienkurs (AlphaVantage)"
- image_generation:
- name: "Flux-Bildgenerator (Together.ai)"
- empty_tool:
- name: "Von vorne beginnen …"
- name:
- characters: "darf nur Zahlen, Buchstaben und Unterstriche enthalten"
- ai_helper:
- errors:
- completion_request_failed: "Beim Versuch, Vorschläge zu machen, ist etwas schiefgelaufen. Bitte versuche es noch einmal."
- prompts:
- translate: Übersetzen in %{language}
- generate_titles: Thementitel vorschlagen
- proofread: Text korrekturlesen
- markdown_table: Markdown-Tabelle generieren
- custom_prompt: "Benutzerdefinierte Eingabeaufforderung"
- explain: "Erklären"
- illustrate_post: "Beitrag illustrieren"
- replace_dates: "Intelligente Termine"
- painter:
- attribution:
- stable_diffusion_xl: "Bild von Stable Diffusion XL"
- dall_e_3: "Bild von DALL-E 3"
- image_caption:
- attribution: "Beschriftet durch KI"
- share_ai:
- read_more: "Vollständiges Transkript lesen"
- onebox_title: "KI-Unterhaltung mit %{llm_name}"
- formatted_excerpt: "KI-Unterhaltung mit %{llm_name}:\n %{excerpt}"
- title: "%{title} – KI-Unterhaltung – %{site_name}"
- errors:
- not_allowed: "Du darfst dieses Thema nicht teilen"
- other_people_in_pm: "Persönliche Nachrichten mit anderen Menschen können nicht öffentlich geteilt werden"
- other_content_in_pm: "Persönliche Nachrichten, die Beiträge von anderen Personen enthalten, können nicht öffentlich geteilt werden"
- failed_to_share: "Die Unterhaltung konnte nicht geteilt werden"
- conversation_deleted: "Unterhaltungsfreigabe erfolgreich gelöscht"
- spam_detection:
- flag_reason: "Als Spam gemeldet von Discourse KI"
- silence_reason: "Benutzer automatisch stummgeschaltet durch Discourse KI"
- invalid_error_type: "Ungültiger Fehlertyp angegeben"
- unexpected: "Ein unerwarteter Fehler ist aufgetreten"
- bot_user_update_failed: "Aktualisierung des Spam-Scan-Bot-Benutzers fehlgeschlagen"
- configuration_missing: "Die Konfiguration der KI-Spamerkennung fehlt. Füge die Konfiguration in „Administration > Plug-ins > Discourse KI > Spam“ hinzu, bevor du sie aktivierst."
- logging_subject: "Spam-Erkennung"
- ai_bot:
- reply_error: "Entschuldigung, es sieht so aus, als ob unser System beim Versuch, zu antworten, auf ein unerwartetes Problem gestoßen ist.\n\n[details='Fehlerdetails']\n%{details}\n[/details]"
- default_pm_prefix: "[KI-Bot-PN ohne Titel]"
- thinking: "Ich denke..."
- personas:
- default_llm_required: "Standard-LLM-Modell ist erforderlich, bevor der Chat aktiviert werden kann"
- cannot_delete_system_persona: "System-Personas können nicht gelöscht werden, bitte deaktiviere sie stattdessen"
- cannot_edit_system_persona: "System-Personas können nur umbenannt werden. Du darfst keine Tools oder die System-Eingabeaufforderung bearbeiten, sondern musst sie deaktivieren und eine Kopie erstellen"
- cannot_have_duplicate_tools: "Es kann keine doppelten Werkzeuge geben"
- github_helper:
- name: "GitHub-Helfer"
- description: "KI-Bot, der auf die Unterstützung bei GitHub-bezogenen Aufgaben und Fragen spezialisiert ist"
- general:
- name: Forum-Helfer
- description: "Universeller KI-Bot, der verschiedene Aufgaben erfüllen kann"
- artist:
- name: Künstler
- description: "KI-Bot, spezialisiert auf die Erstellung von Bildern"
- designer:
- name: Designer
- description: "KI-Bot, spezialisiert auf die Erstellung und Bearbeitung von Bildern"
- forum_researcher:
- name: Forum-Rechercheur
- description: "KI-Bot, spezialisiert auf gründliche Recherche für das Forum"
- sql_helper:
- name: SQL-Helfer
- description: "KI-Bot, der darauf spezialisiert ist, bei der Erstellung von SQL-Abfragen auf dieser Discourse-Instanz zu helfen"
- settings_explorer:
- name: Einstellungs-Explorer
- description: "KI-Bot, der darauf spezialisiert ist, die Einstellungen der Discourse-Website zu erkunden"
- researcher:
- name: Web-Rechercheur
- description: "KI-Bot mit Google-Zugriff, der Webseiten sowohl suchen als auch lesen kann"
- creative:
- name: Kreativ
- description: "KI-Bot ohne externe Integrationen, spezialisiert auf kreative Aufgaben"
- dall_e3:
- name: "DALL-E 3"
- description: "KI-Bot, spezialisiert auf die Erstellung von Bildern mit DALL-E 3"
- discourse_helper:
- name: "Discourse-Helfer"
- description: "KI-Bot, der auf die Unterstützung bei Discourse-bezogenen Aufgaben spezialisiert ist"
- web_artifact_creator:
- name: "Web-Artefakt-Ersteller"
- description: "KI-Bot, spezialisiert auf die Erstellung interaktiver Web-Artefakte"
- summarizer:
- name: "Zusammenfasser"
- description: "Standard-Persona für die KI-Zusammenfassungen"
- short_summarizer:
- name: "Zusammenfasser (Kurzform)"
- description: "Standard-Persona zur Erstellung von KI-Kurzzusammenfassungen für die Elemente der Themenlisten"
- concept_finder:
- name: "Konzept-Finder"
- description: "KI-Bot spezialisiert auf die Identifizierung von Konzepten und Themen in Inhalten"
- concept_matcher:
- name: "Konzept-Verbinder"
- description: "KI-Bot spezialisiert auf den Abgleich von Inhalten mit bestehenden Konzepten"
- concept_deduplicator:
- name: "Konzept-Deduplikator"
- description: "KI-Bot spezialisiert auf das Entfernen von redundanten Daten in Konzepten"
- custom_prompt:
- name: "Benutzerdefinierte Eingabeaufforderung"
- description: "Standard-Persona für die „Benutzerdefinierte Eingabeaufforderungs“-Funktion des Helfers"
- smart_dates:
- name: "Intelligente Termine"
- description: "Standard-Persona für die „Intelligente Termine“-Funktion des Helfers"
- markdown_table_generator:
- name: "Markdown-Tabellengenerator"
- description: "Standard-Persona für die „Generiere Markdown-Tabelle“-Funktion des Helfers"
- post_illustrator:
- name: "Beitragsillustrator"
- description: "Generiert StableDiffusion-Eingabeaufforderungen, für die Funktion „Beitrag illustrieren“ des Helfers"
- proofreader:
- name: "Korrekturleser"
- description: "Standard-Persona für die „Korrekturlesen“-Funktion des Helfers"
- titles_generator:
- name: "Titelgenerator"
- description: "Standard-Persona für die „Thementitel vorschlagen“-Funktion des Helfers"
- tutor:
- name: "Tutor"
- description: "Standard-Persona für die „Erklären“-Funktion des Helfers"
- translator:
- name: "Übersetzer"
- description: "Standard-Persona für die Übersetzerfunktion des Helfers"
- image_captioner:
- name: "Bildbeschriftungen"
- description: "Standard-Persona für die Bildbeschriftungsfunktion des Helfers"
- locale_detector:
- name: "Sprachdetektor"
- description: "Unterstützt die Übersetzungsfunktion durch Erkennen der Sprache eines bestimmten Textes (Beiträge, Titel usw.)."
- post_raw_translator:
- name: "Beitragsübersetzer"
- description: "Unterstützt die Übersetzungsfunktion durch die Übersetzung von Beiträgen, die Discourse Markdown enthalten"
- topic_title_translator:
- name: "Thementitel-Übersetzer"
- description: "Unterstützt die Übersetzungsfunktion durch die Übersetzung von Thementiteln"
- short_text_translator:
- name: "Kurztext-Übersetzer"
- description: "Betreibt die Übersetzungsfunktion als generischer Textübersetzer, der für kurze Texte wie Kategorienamen oder Schlagwörter verwendet wird."
- spam_detector:
- name: "Spam-Detektor"
- description: "Standard-Persona, die unsere Spam-Erkennungsfunktion betreibt"
- content_creator:
- name: "Inhaltsersteller"
- description: "Standard-Persona für die HyDE-Suche"
- topic_not_found: "Zusammenfassung nicht verfügbar, Thema nicht gefunden!"
- summarizing: "Thema zusammenfassen"
- searching: "Suche nach: „%{query}“"
- tool_options:
- researcher:
- researcher_llm:
- name: "LLM"
- description: "Für die Recherche zu verwendendes Sprachmodell (standardmäßig das LLM der aktuellen Persona)"
- max_tokens_per_batch:
- name: "Maximale Token pro Bündel"
- description: "Maximale Anzahl von Token, die für jedes Bündel in der Recherche verwendet werden"
- max_tokens_per_post:
- name: "Maximale Token pro Beitrag"
- description: "Maximale Anzahl von Token, die für jeden Beitrag in der Recherche verwendet werden können"
- max_results:
- name: "Maximale Anzahl von Ergebnissen"
- description: "Maximale Anzahl von Ergebnissen, die in einen Filter aufgenommen werden"
- include_private:
- name: "Private einschließen"
- description: "Private Themen in die Filter einbeziehen"
- create_artifact:
- creator_llm:
- name: "LLM"
- description: "Sprachmodell, das für die Erstellung von Artefakten verwendet werden soll"
- update_artifact:
- editor_llm:
- name: "LLM"
- description: "Sprachmodell, das für Artefaktbearbeitungen verwendet werden soll"
- update_algorithm:
- name: "Algorithmus aktualisieren"
- description: "Bitten LLM, den gesamten Austausch vorzunehmen, oder verwenden diff zum Aktualisieren"
- do_not_echo_artifact:
- name: "Kein Echo Artefakt"
- description: "Begrenzt die Kosten, reduziert aber die Effektivität von Artefakt-Updates"
- google:
- base_query:
- name: "Basissuchanfrage"
- description: "Basisanfrage, die bei der Suche verwendet werden soll. Beispiele: „site:example.com“ enthält nur Ergebnisse von example.com, „before:2022-01-01“ enthält nur Ergebnisse aus dem Jahr 2021 und früher. Dieser Text wird der Suchanfrage vorangestellt."
- read:
- read_private:
- name: "Privat lesen"
- description: "Erlaube den Zugriff auf alle Themen, auf die der Nutzer Zugriff hat (standardmäßig sind nur öffentliche Themen verfügbar)"
- search:
- search_private:
- name: "Privat suchen"
- description: "Alle Themen, auf die der Nutzer Zugriff hat, in die Suchergebnisse einbeziehen (standardmäßig werden nur öffentliche Themen einbezogen)"
- max_results:
- name: "Maximale Anzahl von Ergebnissen"
- description: "Maximale Anzahl der Ergebnisse, die in die Suche einbezogen werden sollen – wenn leer, werden die Standardregeln verwendet und die Anzahl wird je nach verwendetem Modell skaliert. Der höchste Wert ist 100."
- base_query:
- name: "Basissuchanfrage"
- description: "Basisanfrage, die bei der Suche verwendet wird. Beispiel: Bei „#dringend“ wird der Suchanfrage „#dringend“ vorangestellt und es werden nur Themen mit der Kategorie oder dem Schlagwort „dringend“ angezeigt."
- tool_summary:
- read_artifact: "Ein Web-Artefakt lesen"
- update_artifact: "Aktualisieren eines Web-Artefakts"
- create_artifact: "Web-Artefakt erstellen"
- web_browser: "Web durchsuchen"
- github_search_files: "GitHub-Datei-Suche"
- github_search_code: "GitHub-Code-Suche"
- github_file_content: "GitHub-Datei-Inhalt"
- github_pull_request_diff: "GitHub-Pull-Request-Diff"
- random_picker: "Zufallsauswahl"
- categories: "Kategorien auflisten"
- search: "Suche"
- tags: "Schlagwörter auflisten"
- time: "Zeit"
- summarize: "Zusammenfassen"
- image: "Bild generieren"
- google: "Google-Suche"
- read: "Thema lesen"
- setting_context: "Kontext der Website-Einstellung nachschlagen"
- schema: "Datenbankschema nachschlagen"
- search_settings: "Website-Einstellungen werden durchsucht"
- dall_e: "Bild generieren"
- search_meta_discourse: "Meta-Discourse durchsuchen"
- javascript_evaluator: "JavaScript auswerten"
- create_image: "Bild erstellen"
- edit_image: "Bild bearbeiten"
- researcher: "Rechercheur"
- researcher_dry_run: "Recherche vorbereiten"
- tool_help:
- read_artifact: "Lesen eines Webartefakts mit dem KI-Bot"
- update_artifact: "Aktualisiere ein Web-Artefakt mit dem KI-Bot"
- create_artifact: "Erstelle ein Web-Artefakt mit dem KI-Bot"
- web_browser: "Webseite mit dem KI-Bot durchsuchen"
- github_search_code: "Code in einem GitHub-Repository suchen"
- github_search_files: "Dateien in einem GitHub-Repository suchen"
- github_file_content: "Inhalt von Dateien aus einem GitHub-Repository abrufen"
- github_pull_request_diff: "GitHub-Pull-Request-Diff abrufen"
- random_picker: "Zufallszahl oder zufälliges Element aus einer Liste auswählen"
- categories: "Alle öffentlich sichtbaren Kategorien im Forum auflisten"
- search: "Alle öffentlichen Themen im Forum durchsuchen"
- tags: "Alle Schlagwörter im Forum auflisten"
- time: "Zeit in verschiedenen Zeitzonen finden"
- summary: "Ein Thema zusammenfassen"
- image: "Bild mit Stable Diffusion generieren"
- create_image: "Erstelle ein Bild mit dem Open AI GPT Bildmodell"
- edit_image: "Bearbeite ein Bild mit dem Open AI GPT Bildmodell"
- google: "Bei Google nach einer Anfrage suchen"
- read: "Öffentliches Thema im Forum lesen"
- setting_context: "Kontext der Website-Einstellung nachschlagen"
- schema: "Datenbankschema nachschlagen"
- search_settings: "Website-Einstellungen durchsuchen"
- dall_e: "Bild mit DALL-E 3 generieren"
- search_meta_discourse: "Meta-Discourse durchsuchen"
- javascript_evaluator: "JavaScript auswerten"
- researcher: "Foreninformationen mit dem KI-Bot recherchieren"
- tool_description:
- read_artifact: "Lesen eines Webartefakts mit dem KI-Bot"
- update_artifact: "Ein Web-Artefakt wurde mit dem KI-Bot aktualisiert"
- create_artifact: "Erstellt ein Web-Artefakt: %{name} - %{specification}"
- web_browser: "%{url} wird gelesen"
- github_search_files: "Gesucht wurde nach „%{keywords}“ in %{repo}/%{branch}"
- github_search_code: "Gesucht wurde nach „%{query}“ in %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "Inhalt von %{file_paths} in %{repo_name}@%{branch} wurde abgerufen"
- random_picker: "Auswahl aus %{options}. Ausgewählt wurde: %{result}"
- read: "Wird gelesen: %{title}"
- time: "Die Uhrzeit in %{timezone} ist %{time}"
- summarize: "%{title} zusammengefasst"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "%{count} Kategorie gefunden"
- other: "%{count} Kategorien gefunden"
- tags:
- one: "%{count} Schlagwort gefunden"
- other: "%{count} Schlagwörter gefunden"
- search:
- one: "%{count} Ergebnis für „%{query}“ gefunden"
- other: "%{count} Ergebnisse für „%{query}“ gefunden"
- search_meta_discourse:
- one: "%{count} Ergebnis für „%{query}“ gefunden"
- other: "%{count} Ergebnisse für „%{query}“ gefunden"
- google:
- one: "%{count} Ergebnis für „%{query}“ gefunden"
- other: "%{count} Ergebnisse für „%{query}“ gefunden"
- setting_context: "Kontext wird gelesen für: %{setting_name}"
- schema: "%{tables}"
- researcher_dry_run:
- one: "Vorgeschlagene Ziele: %{goals}\n\n%{count} Beitrag passend zu „%{filter}“ gefunden"
- other: "Vorgeschlagene Ziele: %{goals}\n\n%{count} Beiträge passend zu „%{filter}“ gefunden"
- researcher:
- one: "Recherche: %{goals}\n\n%{count} Beitrag passend zu „%{filter}“ gefunden"
- other: "Recherche: %{goals}\n\n%{count} Beiträge passend zu „%{filter}“ gefunden"
- search_settings:
- one: "%{count} Ergebnis für „%{query}“ gefunden"
- other: "%{count} Ergebnisse für „%{query}“ gefunden"
- discoveries:
- continue_conversation:
- title: "Entdeckungsunterhaltung: Suche nach %{query}"
- raw: "Bei meiner Suche nach %{query} hast du mir die folgenden Informationen gezeigt:\n\n%{context}\n\nLass uns die Unterhaltung fortsetzen."
- summarization:
- configuration_hint:
- one: "Konfiguriere zunächst die Einstellung `%{setting}`."
- other: "Konfiguriere zunächst diese Einstellungen: %{settings}"
- chat:
- no_targets: "Im ausgewählten Zeitraum gab es keine Nachrichten."
- sentiment:
- reports:
- overall_sentiment: "Gesamtstimmung (positiv–negativ)"
- post_emotion:
- sadness: "Traurigkeit \U0001F622"
- surprise: "Überraschung \U0001F631"
- neutral: "Neutral \U0001F610"
- fear: "Furcht \U0001F628"
- anger: "Wut \U0001F621"
- joy: "Freude \U0001F600"
- disgust: "Ekel \U0001F922"
- sentiment_analysis:
- positive: "Positiv"
- negative: "Negativ"
- neutral: "Neutral"
- llm:
- configuration:
- create_llm: "Du musst einen LLM einrichten, bevor du diese Funktion aktivierst"
- disable_module_first: "Du musst zuerst %{setting} deaktivieren."
- set_llm_first: "Stelle zuerst %{setting} ein"
- model_unreachable: "Wir konnten keine Antwort von diesem Modell abrufen. Überprüfe zuerst deine Einstellungen."
- invalid_seeded_model: "Du kannst dieses Modell nicht mit dieser Funktion verwenden"
- invalid_persona_response_format: "Die ausgewählte Persona muss ein Antwortformat mit einem booleschen Feld namens „spam“ haben."
- must_select_model: "Du musst zuerst ein LLM auswählen"
- endpoints:
- not_configured: "%{display_name} (nicht konfiguriert)"
- configuration_hint:
- one: "Vergewissere dich, dass die Einstellung \"%{settings}\" konfiguriert wurde."
- other: "Vergewissere dich, dass diese Einstellungen konfiguriert wurden: %{settings}"
- delete_failed:
- one: "Wir konnten dieses Modell nicht löschen, weil es von %{settings} verwendet wird. Aktualisiere die Einstellung und versuche es erneut."
- other: "Wir konnten dieses Modell nicht löschen, weil %{settings} es verwenden. Aktualisiere die Einstellungen und versuche es erneut."
- cannot_edit_builtin: "Du kannst ein integriertes Modell nicht bearbeiten."
- personas:
- malformed_examples: "Die angegebenen Beispiele haben das falsche Format."
- embeddings:
- delete_failed: "Dieses Modell wird derzeit verwendet. Aktualisiere zuerst `ai embeddings selected model`."
- cannot_edit_builtin: "Du kannst ein integriertes Modell nicht bearbeiten."
- configuration:
- disable_embeddings: "Du musst zuerst „KI-Einbettungen aktiviert“ deaktivieren."
- invalid_config: "Du hast eine ungültige Option ausgewählt."
- choose_model: "Lege zuerst „ausgewähltes KI-Einbettungsmodell“ fest."
- llm_models:
- missing_provider_param: "%{param} darf nicht leer sein"
- bedrock_invalid_url: "Bitte fülle alle Felder aus, um dieses Modell zu verwenden."
- ai_staff_action_logger:
- updated: "aktualisiert"
- set: "gesetzt"
- removed: "entfernt"
- errors:
- quota_exceeded: "Du hast das Kontingent für dieses Modell überschritten. Bitte versuche es erneut in %{relative_time}."
- quota_required: "Du musst die maximale Anzahl an Token oder Verwendungen für dieses Modell angeben"
- no_query_specified: Der Abfrageparameter ist erforderlich, bitte gib ihn an.
- no_user_for_persona: Die angegebene Persona hat keinen Benutzer, der mit ihr verbunden ist.
- persona_not_found: Die angegebene Persona existiert nicht. Überprüfe die Parameter persona_name oder persona_id.
- no_user_specified: Der Benutzername oder der Parameter user_unique_id ist erforderlich. Bitte gib ihn an.
- user_not_found: Der angegebene Benutzer existiert nicht. Überprüfe den Parameter username.
- persona_disabled: Die angegebene Persona ist deaktiviert. Überprüfe die Parameter persona_name oder persona_id.
- no_default_llm: Die Persona muss eine default_llm definiert haben.
- user_not_allowed: Der Benutzer darf nicht am Thema teilnehmen.
- prompt_message_length: Die Nachricht %{idx} hat die 1000-Zeichen-Grenze überschritten.
- persona_already_exists: Eine Persona mit dem Namen %{name} existiert bereits.
- custom_tool_exists:
- one: "Ein benutzerdefiniertes Tool mit dem Namen %{names} existiert bereits."
- other: "Benutzerdefinierte Tools mit den Namen %{names} existieren bereits."
- dashboard:
- problem:
- ai_llm_status: "Bei dem LLM-Modell: %{model_name} treten Probleme auf. Bitte überprüfe die Konfigurationsseite des Modells."
diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml
deleted file mode 100644
index bbb69301..00000000
--- a/config/locales/server.el.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-el:
- reports:
- overall_sentiment:
- yaxis: "Ημερομηνία"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Αναζήτηση"
- time: "Ώρα"
- ai_staff_action_logger:
- updated: "ενημερώθηκε"
- removed: "αφαιρέθηκε"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
deleted file mode 100644
index 5a869659..00000000
--- a/config/locales/server.en.yml
+++ /dev/null
@@ -1,640 +0,0 @@
-en:
- discourse_automation:
- ai:
- flag_types:
- review: "Add post to review queue"
- review_hide: "Add post to review queue and hide post"
- spam: "Flag as spam and hide post"
- spam_silence: "Flag as spam, hide post and silence user"
- scriptables:
- llm_tool_triage:
- title: Triage posts using AI Tool
- description: "Triage posts using custom logic in an AI tool"
- llm_persona_triage:
- title: Triage posts using AI Persona
- description: "Respond to posts using a specific AI persona"
- llm_triage:
- title: Triage posts using AI
- description: "Triage posts using a large language model"
- flagged_post: |
-
Response from the model:
-
%%LLM_RESPONSE%%
- Triggered by the %%AUTOMATION_NAME%% rule.
- llm_report:
- title: Periodic report using AI
- description: "Periodic report based on a large language model"
- site_settings:
- discourse_ai_enabled: "Enable the discourse AI plugin."
- ai_artifact_security: "The AI artifact system generates IFRAMEs with runnable code. Strict mode forces an extra click to run code. Lax mode runs code immediately. Hybrid mode allows user to supply data-ai-artifact-autorun to show right away. Disabled mode disables the artifact system."
- ai_toxicity_enabled: "Enable the toxicity module."
- ai_toxicity_inference_service_api_endpoint: "URL where the API is running for the toxicity module"
- ai_toxicity_inference_service_api_key: "API key for the toxicity API"
- ai_toxicity_inference_service_api_model: "Model to use for inference. Multilingual model works with Italian, French, Russian, Portuguese, Spanish and Turkish."
- ai_toxicity_flag_automatically: "Automatically flag posts / chat messages that are above the configured thresholds."
- ai_toxicity_flag_threshold_toxicity: "Toxicity: a rude, disrespectful, or unreasonable comment that is somewhat likely to make you leave a discussion or give up on sharing your perspective"
- ai_toxicity_flag_threshold_severe_toxicity: "Severe Toxicity: a very hateful, aggressive, or disrespectful comment that is very likely to make you leave a discussion or give up on sharing your perspective"
- ai_toxicity_flag_threshold_obscene: "Obscene"
- ai_toxicity_flag_threshold_identity_attack: "Identity Attack"
- ai_toxicity_flag_threshold_insult: "Insult"
- ai_toxicity_flag_threshold_threat: "Threat"
- ai_toxicity_flag_threshold_sexual_explicit: "Sexual Explicit"
- ai_toxicity_groups_bypass: "Users on those groups will not have their posts classified by the toxicity module."
-
- ai_sentiment_enabled: "Enable the sentiment module."
- ai_sentiment_inference_service_api_endpoint: "URL where the API is running for the sentiment module"
- ai_sentiment_inference_service_api_key: "API key for the sentiment API"
- ai_sentiment_models: "Models to use for inference. Sentiment classifies post on the positive/neutral/negative space. Emotion classifies on the anger/disgust/fear/joy/neutral/sadness/surprise space."
-
- ai_nsfw_detection_enabled: "Enable the NSFW module."
- ai_nsfw_inference_service_api_endpoint: "URL where the API is running for the NSFW module"
- ai_nsfw_inference_service_api_key: "API key for the NSFW API"
- ai_nsfw_flag_automatically: "Automatically flag NSFW posts that are above the configured thresholds."
- ai_nsfw_flag_threshold_general: "General Threshold for an image to be considered NSFW."
- ai_nsfw_flag_threshold_drawings: "Threshold for a drawing to be considered NSFW."
- ai_nsfw_flag_threshold_hentai: "Threshold for an image classified as hentai to be considered NSFW."
- ai_nsfw_flag_threshold_porn: "Threshold for an image classified as porn to be considered NSFW."
- ai_nsfw_flag_threshold_sexy: "Threshold for an image classified as sexy to be considered NSFW."
- ai_nsfw_models: "Models to use for NSFW inference."
-
- ai_spam_detection_enabled: "Enable the AI spam detection module"
-
- ai_openai_api_key: "API key for OpenAI API. ONLY used for Image creation and edits. For GPT use the LLM config tab"
- ai_openai_image_generation_url: "URL for OpenAI image generation API"
- ai_openai_image_edit_url: "URL for OpenAI image edit API"
-
- ai_helper_enabled: "Enable the AI helper."
- composer_ai_helper_allowed_groups: "Users on these groups will see the AI helper button in the composer."
- ai_helper_allowed_in_pm: "Enable the composer's AI helper in PMs."
- ai_helper_model: "Model to use for the AI helper."
- ai_helper_custom_prompts_allowed_groups: "Users on these groups will see the custom prompt option in the AI helper."
- ai_helper_automatic_chat_thread_title_delay: "Delay in minutes before the AI helper automatically sets the chat thread title."
- ai_helper_automatic_chat_thread_title: "Automatically set the chat thread titles based on thread contents."
- ai_helper_illustrate_post_model: "Model to use for the composer AI helper's illustrate post feature"
- ai_helper_enabled_features: "Select the features to enable in the AI helper."
- post_ai_helper_allowed_groups: "User groups allowed to access AI Helper features in posts"
- ai_helper_image_caption_model: "Select the model to use for generating image captions"
- ai_auto_image_caption_allowed_groups: "Users on these groups can toggle automatic image captioning."
-
- ai_embeddings_selected_model: "Use the selected model for generating embeddings."
- ai_embeddings_generate_for_pms: "Generate embeddings for personal messages."
- ai_embeddings_semantic_related_topics_enabled: "Use Semantic Search for related topics."
- ai_embeddings_semantic_related_topics: "Maximum number of topics to show in related topic section."
- ai_embeddings_backfill_batch_size: "Number of embeddings to backfill every 15 minutes."
- ai_embeddings_semantic_search_enabled: "Enable full-page semantic search."
- ai_embeddings_semantic_quick_search_enabled: "Enable semantic search option in search menu popup."
- ai_embeddings_semantic_related_include_closed_topics: "Include closed topics in semantic search results"
- ai_embeddings_semantic_search_hyde_model: "Model used to expand keywords to get better results during a semantic search"
- ai_embeddings_per_post_enabled: Generate embeddings for each post
-
- ai_summarization_enabled: "Enable the summarize feature"
- ai_summarization_model: "Model to use for summarization"
- ai_summarization_persona: "Persona to use for summarize feature"
- ai_custom_summarization_allowed_groups: "Groups allowed to use create new summaries."
- ai_pm_summarization_allowed_groups: "Groups allowed to create and view summaries in PMs."
- ai_summary_gists_enabled: "Generate brief summaries of latest replies in topics automatically"
- ai_summary_gists_allowed_groups: "Groups allowed to see gists in the hot topics list."
- ai_summary_backfill_maximum_topics_per_hour: "Number of topic summaries to backfill per hour."
-
- ai_bot_enabled: "Enable the AI Bot module."
- ai_bot_enable_chat_warning: "Display a warning when PM chat is initiated. Can be overriden by editing the translation string: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "When the GPT Bot has access to the PM, it will reply to members of these groups."
- ai_bot_debugging_allowed_groups: "Allow these groups to see a debug button on posts which displays the raw AI request and response"
- ai_bot_public_sharing_allowed_groups: "Allow these groups to share AI personal messages with the public via a unique publicly available link. Note: if your site requires login, shares will also require login."
- ai_bot_add_to_header: "Display a button in the header to start a PM with a AI Bot"
- ai_bot_github_access_token: "GitHub access token for use with GitHub AI tools (required for search support)"
-
- ai_stability_api_key: "API key for the stability.ai API"
- ai_stability_engine: "Image generation engine to use for the stability.ai API"
- ai_stability_api_url: "URL for the stability.ai API"
-
- ai_google_custom_search_api_key: "API key for the Google Custom Search API see: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX for Google Custom Search API"
-
- ai_discord_search_enabled: "Enables the Discord search feature"
- ai_discord_app_id: "The ID of the Discord application you would like to connect Discord search to"
- ai_discord_app_public_key: "The public key of the Discord application you would like to connect Discord search to"
- ai_discord_search_mode: "Select the search mode to use for Discord search"
- ai_discord_search_persona: "The persona to use for Discord search."
- ai_discord_allowed_guilds: "Discord guilds (servers) where the bot is allowed to search"
- ai_bot_enable_dedicated_ux: "Allow for full screen bot interface, instead of a PM"
-
- ai_translation_enabled: "Enables the AI translation feature"
- ai_translation_model: "The model to use for translation. This model must support translation. Personas can override this setting."
- ai_translation_backfill_limit_to_public_content: "When enabled, only content in public categories will be translated. When disabled, content in group PMs and private categories will also be sent for translation."
- ai_translation_max_post_length: "The maximum length of a post to be translated. Posts longer than this will not be translated."
- ai_translation_backfill_max_age_days: "The maximum age of a post and topic to be translated. Posts and topics older than this will not be translated. 0 disables backfilling, but will not disable translation of new posts."
-
- reviewables:
- reasons:
- flagged_by_toxicity: The AI plugin flagged this after classifying it as toxic.
- flagged_by_nsfw: The AI plugin flagged this after classifying at least one of the attached images as NSFW.
-
- reports:
- sentiment_analysis:
- title: "Sentiment analysis"
- description: "This report provides sentiment analysis for posts, grouped by category, with positive, negative, and neutral scores for each post and category."
- overall_sentiment:
- title: "Overall sentiment"
- description: 'The chart compares the number of posts classified as either positive or negative. These are calculated when positive or negative scores > the set threshold score. This means neutral posts are not shown. Personal messages (PMs) are also excluded. Classified with "cardiffnlp/twitter-roberta-base-sentiment-latest"'
- xaxis: "Positive(%)"
- yaxis: "Date"
- emotion_admiration:
- title: 🤩 Admiration
- description: "Posts classified with the emotion admiration via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_amusement:
- title: 😄 Amusement
- description: "Posts classified with the emotion amusement via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_anger:
- title: 😠 Anger
- description: "Posts classified with the emotion anger via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_annoyance:
- title: 😒 Annoyance
- description: "Posts classified with the emotion annoyance via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_approval:
- title: 👍 Approval
- description: "Posts classified with the emotion approval via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_caring:
- title: 🤗 Caring
- description: "Posts classified with the emotion caring via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_confusion:
- title: 😕 Confusion
- description: "Posts classified with the emotion confusion via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_curiosity:
- title: 🤔 Curiosity
- description: "Posts classified with the emotion curiosity via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_desire:
- title: 😍 Desire
- description: "Posts classified with the emotion desire via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_disappointment:
- title: 😞 Disappointment
- description: "Posts classified with the emotion disappointment via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_disapproval:
- title: 👎 Disapproval
- description: "Posts classified with the emotion disapproval via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_disgust:
- title: 🤢 Disgust
- description: "Posts classified with the emotion disgust via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_embarrassment:
- title: 😳 Embarrassment
- description: "Posts classified with the emotion embarrassment via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_excitement:
- title: 🤪 Excitement
- description: "Posts classified with the emotion excitement via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_fear:
- title: 😨 Fear
- description: "Posts classified with the emotion fear via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_gratitude:
- title: 🙏 Gratitude
- description: "Posts classified with the emotion gratitude via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_grief:
- title: 😢 Grief
- description: "Posts classified with the emotion grief via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_joy:
- title: 😊 Joy
- description: "Posts classified with the emotion joy via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_love:
- title: ❤️ Love
- description: "Posts classified with the emotion love via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_nervousness:
- title: 😰 Nervousness
- description: "Posts classified with the emotion nervousness via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_neutral:
- title: 😐 Neutral
- description: "Posts classified with the emotion neutral via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_optimism:
- title: 🌟 Optimism
- description: "Posts classified with the emotion optimism via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_pride:
- title: 🦁 Pride
- description: "Posts classified with the emotion pride via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_realization:
- title: 💡 Realization
- description: "Posts classified with the emotion realization via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_relief:
- title: 😌 Relief
- description: "Posts classified with the emotion relief via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_remorse:
- title: 😔 Remorse
- description: "Posts classified with the emotion remorse via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_sadness:
- title: 😭 Sadness
- description: "Posts classified with the emotion sadness via AI, using the model 'SamLowe/roberta-base-go_emotions'."
- emotion_surprise:
- title: 😲 Surprise
- description: "Posts classified with the emotion surprise via AI, using the model 'SamLowe/roberta-base-go_emotions'."
-
- discourse_ai:
- ai_artifact:
- errors:
- max_keys_exceeded:
- one: "You can only have %{count} key in the artifact."
- other: "You can only have %{count} keys in the artifact."
- link: "Show in new tab"
- copy_embed: "Copy embed"
- view_source: "View Source"
- view_changes: "View Changes"
- change_description: "Change Description"
- copied: "Copied to clipboard"
- unknown_model: "Unknown AI model"
-
- tools:
- custom_name: "%{name} (custom)"
- presets:
- browse_web_jina:
- name: "Browse web (jina.ai)"
- exchange_rate:
- name: "Exchange rate"
- stock_quote:
- name: "Stock quote (AlphaVantage)"
- image_generation:
- name: "Flux image generator (Together.ai)"
- empty_tool:
- name: "Start from blank..."
- name:
- characters: "must only include numbers, letters, and underscores"
-
- ai_helper:
- errors:
- completion_request_failed: "Something went wrong while trying to provide suggestions. Please, try again."
- prompts:
- translate: Translate to %{language}
- generate_titles: Suggest topic titles
- proofread: Proofread text
- markdown_table: Generate Markdown table
- custom_prompt: "Custom Prompt"
- explain: "Explain"
- illustrate_post: "Illustrate Post"
- replace_dates: "Smart dates"
- painter:
- attribution:
- stable_diffusion_xl: "Image by Stable Diffusion XL"
- dall_e_3: "Image by DALL-E 3"
- image_caption:
- attribution: "Captioned by AI"
-
- share_ai:
- read_more: "Read full transcript"
- onebox_title: "AI Conversation with %{llm_name}"
- formatted_excerpt: "AI Conversation with %{llm_name}:\n %{excerpt}"
- title: "%{title} - AI Conversation - %{site_name}"
- errors:
- not_allowed: "You are not allowed to share this topic"
- other_people_in_pm: "Personal messages with other humans cannot be shared publicly"
- other_content_in_pm: "Personal messages containing posts from other people cannot be shared publicly"
- failed_to_share: "Failed to share the conversation"
- conversation_deleted: "Conversation share deleted successfully"
- spam_detection:
- flag_reason: "Flagged as spam by Discourse AI"
- silence_reason: "User silenced automatically by Discourse AI"
- invalid_error_type: "Invalid error type provided"
- unexpected: "An unexpected error occured"
- bot_user_update_failed: "Failed to update the spam scanning bot user"
- configuration_missing: "The AI spam detection configuration is missing. Add configuration in the 'Admin > Plugins > Discourse AI > Spam' before enabling."
- logging_subject: "Spam detection"
-
- ai_bot:
- reply_error: "Sorry, it looks like our system encountered an unexpected issue while trying to reply.\n\n[details='Error details']\n%{details}\n[/details]"
- default_pm_prefix: "[Untitled AI bot PM]"
- thinking: "Thinking..."
- personas:
- default_llm_required: "Default LLM model is required prior to enabling Chat"
- cannot_delete_system_persona: "System personas cannot be deleted, please disable it instead"
- cannot_edit_system_persona: "System personas can only be renamed, you may not edit tools or system prompt, instead disable and make a copy"
- cannot_have_duplicate_tools: "Can not have duplicate tools"
- github_helper:
- name: "GitHub Helper"
- description: "AI Bot specialized in assisting with GitHub-related tasks and questions"
- general:
- name: Forum Helper
- description: "General purpose AI Bot capable of performing various tasks"
- artist:
- name: Artist
- description: "AI Bot specialized in generating images"
- designer:
- name: Designer
- description: "AI Bot specialized in generating and editing images"
- forum_researcher:
- name: Forum Researcher
- description: "AI Bot specialized in deep research for the forum"
- sql_helper:
- name: SQL Helper
- description: "AI Bot specialized in helping craft SQL queries on this Discourse instance"
- settings_explorer:
- name: Settings Explorer
- description: "AI Bot specialized in helping explore Discourse site settings"
- researcher:
- name: Web Researcher
- description: "AI Bot with Google access that can both search and read web pages"
- creative:
- name: Creative
- description: "AI Bot with no external integrations specialized in creative tasks"
- dall_e3:
- name: "DALL-E 3"
- description: "AI Bot specialized in generating images using DALL-E 3"
- discourse_helper:
- name: "Discourse Helper"
- description: "AI Bot specialized in helping with Discourse related tasks"
- web_artifact_creator:
- name: "Web Artifact Creator"
- description: "AI Bot specialized in creating interactive web artifacts"
- summarizer:
- name: "Summarizer"
- description: "Default persona used to power AI summaries"
- short_summarizer:
- name: "Summarizer (short form)"
- description: "Default persona used to power AI short summaries for topic lists' items"
- concept_finder:
- name: "Concept Finder"
- description: "AI Bot specialized in identifying concepts and themes in content"
- concept_matcher:
- name: "Concept Matcher"
- description: "AI Bot specialized in matching content against existing concepts"
- concept_deduplicator:
- name: "Concept Deduplicator"
- description: "AI Bot specialized in deduplicating concepts"
- custom_prompt:
- name: "Custom prompt"
- description: "Default persona powering the Helper's custom prompt feature"
- smart_dates:
- name: "Smart dates"
- description: "Default persona powering the Helper's smart dates feature"
- markdown_table_generator:
- name: "Markdown table generator"
- description: "Default persona powering the Helper's generate Markdown table feature"
- post_illustrator:
- name: "Post illustrator"
- description: "Generates StableDiffusion prompts to power the Helper's illustrate post feature"
- proofreader:
- name: "Proofreader"
- description: "Default persona powering the Helper's proofread text feature"
- titles_generator:
- name: "Titles generator"
- description: "Default persona powering the Helper's suggest topic titles feature"
- tutor:
- name: "Tutor"
- description: "Default persona powering the Helper's explain feature"
- translator:
- name: "Translator"
- description: "Default persona powering the Helper's translator feature"
- image_captioner:
- name: "Image captions"
- description: "Default persona powering the Helper's image caption feature"
- locale_detector:
- name: "Locale detector"
- description: "Powers the translation feature by detecting the locale of a given text (posts, titles, etc.)"
- post_raw_translator:
- name: "Post translator"
- description: "Powers the translation feature by translating posts containing Discourse Markdown"
- topic_title_translator:
- name: "Topic title translator"
- description: "Powers the translation feature by translating topic titles"
- short_text_translator:
- name: "Short text translator"
- description: "Powers the translation feature by as a generic text translator, used for short texts like category names or tags"
- spam_detector:
- name: "Spam detector"
- description: "Default persona powering our Spam detection feature"
- content_creator:
- name: "Content creator"
- description: "Default persona powering HyDE search"
- report_runner:
- name: "Report runner"
- description: "Default persona used in the report automation script"
-
- topic_not_found: "Summary unavailable, topic not found!"
- summarizing: "Summarizing topic"
- searching: "Searching for: '%{query}'"
- tool_options:
- researcher:
- researcher_llm:
- name: "LLM"
- description: "Language model to use for research (default to current persona's LLM)"
- max_tokens_per_batch:
- name: "Maximum tokens per batch"
- description: "Maximum number of tokens to use for each batch in the research"
- max_tokens_per_post:
- name: "Maximum tokens per post"
- description: "Maximum number of tokens to use for each post in the research"
- max_results:
- name: "Maximum number of results"
- description: "Maximum number of results to include in a filter"
- include_private:
- name: "Include private"
- description: "Include private topics in the filters"
- create_artifact:
- creator_llm:
- name: "LLM"
- description: "Language model to use for artifact creation"
- update_artifact:
- editor_llm:
- name: "LLM"
- description: "Language model to use for artifact edits"
- update_algorithm:
- name: "Update Algorithm"
- description: "Ask LLM to fully replace, or use diff to update"
- do_not_echo_artifact:
- name: "Do Not Echo Artifact"
- description: "Will limit costs however effectiveness of Artifact updates will be reduced"
- google:
- base_query:
- name: "Base Search Query"
- description: "Base query to use when searching. Examples: 'site:example.com' will only include results from example.com, before:2022-01-01 will only includes results from 2021 and earlier. This text is prepended to the search query."
- read:
- read_private:
- name: "Read Private"
- description: "Allow access to all topics user has access to (by default only public topics are included)"
- search:
- search_private:
- name: "Search Private"
- description: "Include all topics user has access to in search results (by default only public topics are included)"
- max_results:
- name: "Maximum number of results"
- description: "Maximum number of results to include in the search - if empty default rules will be used and count will be scaled depending on model used. Highest value is 100."
- base_query:
- name: "Base Search Query"
- description: "Base query to use when searching. Example: '#urgent' will prepend '#urgent' to the search query and only include topics with the urgent category or tag."
- tool_summary:
- read_artifact: "Read a web artifact"
- update_artifact: "Update a web artifact"
- create_artifact: "Create web artifact"
- web_browser: "Browse Web"
- github_search_files: "GitHub search files"
- github_search_code: "GitHub code search"
- github_file_content: "GitHub file content"
- github_pull_request_diff: "GitHub pull request diff"
- random_picker: "Random Picker"
- categories: "List categories"
- search: "Search"
- tags: "List tags"
- time: "Time"
- summarize: "Summarize"
- image: "Generate image"
- google: "Search Google"
- read: "Read topic"
- setting_context: "Look up site setting context"
- schema: "Look up database schema"
- search_settings: "Searching site settings"
- dall_e: "Generate image"
- search_meta_discourse: "Search Meta Discourse"
- javascript_evaluator: "Evaluate JavaScript"
- create_image: "Creating image"
- edit_image: "Editing image"
- researcher: "Researching"
- researcher_dry_run: "Preparing research"
- tool_help:
- read_artifact: "Read a web artifact using the AI Bot"
- update_artifact: "Update a web artifact using the AI Bot"
- create_artifact: "Create a web artifact using the AI Bot"
- web_browser: "Browse web page using the AI Bot"
- github_search_code: "Search for code in a GitHub repository"
- github_search_files: "Search for files in a GitHub repository"
- github_file_content: "Retrieve content of files from a GitHub repository"
- github_pull_request_diff: "Retrieve a GitHub pull request diff"
- random_picker: "Pick a random number or a random element of a list"
- categories: "List all publicly visible categories on the forum"
- search: "Search all public topics on the forum"
- tags: "List all tags on the forum"
- time: "Find time in various time zones"
- summary: "Summarize a topic"
- image: "Generate image using Stable Diffusion"
- create_image: "Generate image using Open AI GPT image model"
- edit_image: "Edit image using Open AI GPT image model"
- google: "Search Google for a query"
- read: "Read public topic on the forum"
- setting_context: "Look up site setting context"
- schema: "Look up database schema"
- search_settings: "Search site settings"
- dall_e: "Generate image using DALL-E 3"
- search_meta_discourse: "Search Meta Discourse"
- javascript_evaluator: "Evaluate JavaScript"
- researcher: "Research forum information using the AI Bot"
- tool_description:
- read_artifact: "Read a web artifact using the AI Bot"
- update_artifact: "Updated a web artifact using the AI Bot"
- create_artifact: "Created a web artifact: %{name} - %{specification}"
- web_browser: "Reading %{url}"
- github_search_files: "Searched for '%{keywords}' in %{repo}/%{branch}"
- github_search_code: "Searched for '%{query}' in %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "Retrieved content of %{file_paths} from %{repo_name}@%{branch}"
- random_picker: "Picking from %{options}, picked: %{result}"
- read: "Reading: %{title}"
- time: "Time in %{timezone} is %{time}"
- summarize: "Summarized %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "Found %{count} category"
- other: "Found %{count} categories"
- tags:
- one: "Found %{count} tag"
- other: "Found %{count} tags"
- search:
- one: "Found %{count} result for '%{query}'"
- other: "Found %{count} results for '%{query}'"
- search_meta_discourse:
- one: "Found %{count} result for '%{query}'"
- other: "Found %{count} results for '%{query}'"
- google:
- one: "Found %{count} result for '%{query}'"
- other: "Found %{count} results for '%{query}'"
- setting_context: "Reading context for: %{setting_name}"
- schema: "%{tables}"
- researcher_dry_run:
- one: "Proposed goals: %{goals}\n\nFound %{count} post matching '%{filter}'"
- other: "Proposed goals: %{goals}\n\nFound %{count} posts matching '%{filter}'"
- researcher:
- one: "Researching: %{goals}\n\nFound %{count} post matching '%{filter}'"
- other: "Researching: %{goals}\n\nFound %{count} posts matching '%{filter}'"
- search_settings:
- one: "Found %{count} result for '%{query}'"
- other: "Found %{count} results for '%{query}'"
- discoveries:
- continue_conversation:
- title: "Discovery conversation: Search for %{query}"
- raw: "In my search for %{query}, you showed me the following information:\n\n%{context}\n\nLet's continue the conversation."
-
- summarization:
- configuration_hint:
- one: "Configure the `%{setting}` setting first."
- other: "Configure these settings first: %{settings}"
- chat:
- no_targets: "There were no messages during the selected period."
-
- sentiment:
- reports:
- overall_sentiment: "Overall sentiment (Positive - Negative)"
- post_emotion:
- sadness: "Sadness 😢"
- surprise: "Surprise 😱"
- neutral: "Neutral 😐"
- fear: "Fear 😨"
- anger: "Anger 😡"
- joy: "Joy 😀"
- disgust: "Disgust 🤢"
- sentiment_analysis:
- positive: "Positive"
- negative: "Negative"
- neutral: "Neutral"
-
- llm:
- configuration:
- create_llm: "You need to setup an LLM before enabling this feature"
- disable_module_first: "You have to disable %{setting} first."
- set_llm_first: "Set %{setting} first"
- model_unreachable: "We couldn't get a response from this model. Check your settings first."
- invalid_seeded_model: "You can't use this model with this feature"
- invalid_persona_response_format: "The selected persona must have a response format with a boolean field names \"spam\""
- must_select_model: "You must select a LLM first"
- endpoints:
- not_configured: "%{display_name} (not configured)"
- configuration_hint:
- one: "Make sure the `%{settings}` setting was configured."
- other: "Make sure these settings were configured: %{settings}"
-
- delete_failed:
- one: "We couldn't delete this model because %{settings} is using it. Update the setting and try again."
- other: "We couldn't delete this model because %{settings} are using it. Update the settings and try again."
- cannot_edit_builtin: "You can't edit a built-in model."
-
- personas:
- malformed_examples: "The given examples have the wrong format."
-
- embeddings:
- delete_failed: "This model is currently in use. Update the `ai embeddings selected model` first."
- cannot_edit_builtin: "You can't edit a built-in model."
- configuration:
- disable_embeddings: "You have to disable 'ai embeddings enabled' first."
- invalid_config: "You selected a invalid option."
- choose_model: "Set 'ai embeddings selected model' first."
-
- llm_models:
- missing_provider_param: "%{param} can't be blank"
- bedrock_invalid_url: "Please complete all the fields to use this model."
-
- ai_staff_action_logger:
- updated: "updated"
- set: "set"
- removed: "removed"
-
- errors:
- quota_exceeded: "You have exceeded the quota for this model. Please try again in %{relative_time}."
- quota_required: "You must specify maximum tokens or usages for this model"
- no_query_specified: The query parameter is required, please specify it.
- no_user_for_persona: The persona specified does not have a user associated with it.
- persona_not_found: The persona specified does not exist. Check the persona_name or persona_id params.
- no_user_specified: The username or the user_unique_id parameter is required, please specify it.
- user_not_found: The user specified does not exist. Check the username param.
- persona_disabled: The persona specified is disabled. Check the persona_name or persona_id params.
- no_default_llm: The persona must have a default_llm defined.
- user_not_allowed: The user is not allowed to participate in the topic.
- prompt_message_length: The message %{idx} is over the 1000 character limit.
- persona_already_exists: Persona with the name %{name} already exists.
- custom_tool_exists:
- one: "Custom tool with the name %{names} already exists."
- other: "Custom tools with the names %{names} already exist."
- dashboard:
- problem:
- ai_llm_status: "The LLM model: %{model_name} is encountering issues. Please check the model's configuration page."
diff --git a/config/locales/server.en_GB.yml b/config/locales/server.en_GB.yml
deleted file mode 100644
index 2d4fa180..00000000
--- a/config/locales/server.en_GB.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-en_GB:
diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml
deleted file mode 100644
index 660b623e..00000000
--- a/config/locales/server.es.yml
+++ /dev/null
@@ -1,445 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-es:
- discourse_automation:
- ai:
- flag_types:
- review: "Añadir publicación a la cola de revisión"
- spam: "Marcar como spam y ocultar publicación"
- spam_silence: "Marcar como spam, ocultar publicación y silenciar al usuario"
- scriptables:
- llm_triage:
- title: Triaje de publicaciones mediante IA
- description: "Triaje de publicaciones utilizando un gran modelo lingüístico"
- flagged_post: |
-
Respuesta del modelo:
-
%%LLM_RESPONSE%%
- Activada por la regla.
- llm_report:
- title: Informe periódico mediante IA
- description: "Informe periódico basado en un modelo lingüístico de gran tamaño"
- site_settings:
- discourse_ai_enabled: "Activar el complemento de IA de Discourse."
- ai_toxicity_enabled: "Activar el módulo de toxicidad."
- ai_toxicity_inference_service_api_endpoint: "URL donde se ejecuta la API para el módulo de toxicidad"
- ai_toxicity_inference_service_api_key: "Clave API para la API de toxicidad"
- ai_toxicity_inference_service_api_model: "Modelo que se utilizará para la inferencia. El modelo multilingüe funciona con italiano, francés, ruso, portugués, español y turco."
- ai_toxicity_flag_automatically: "Denunciar automáticamente las publicaciones / mensajes de chat que superen los umbrales configurados."
- ai_toxicity_flag_threshold_toxicity: "Toxicidad: un comentario grosero, irrespetuoso o irrazonable que tiene ciertas probabilidades de hacerte abandonar una discusión o renunciar a compartir tu punto de vista."
- ai_toxicity_flag_threshold_severe_toxicity: "Toxicidad grave: un comentario muy odioso, agresivo o irrespetuoso que es muy probable que te haga abandonar una discusión o renunciar a compartir tu punto de vista."
- ai_toxicity_flag_threshold_obscene: "Obsceno"
- ai_toxicity_flag_threshold_identity_attack: "Ataque de identidad"
- ai_toxicity_flag_threshold_insult: "Insulto"
- ai_toxicity_flag_threshold_threat: "Amenaza"
- ai_toxicity_flag_threshold_sexual_explicit: "Sexual explícito"
- ai_toxicity_groups_bypass: "Los usuarios de esos grupos no verán sus mensajes clasificados por el módulo de toxicidad."
- ai_sentiment_enabled: "Activar el módulo de sentimientos."
- ai_sentiment_inference_service_api_endpoint: "URL donde se ejecuta la API para el módulo de sentimientos"
- ai_sentiment_inference_service_api_key: "Clave API para la API de sentimientos"
- ai_sentiment_models: "Modelos que se utilizarán para la inferencia. El sentimiento clasifica las publicaciones en el espacio positivo/neutral/negativo. La emoción se clasifica en el espacio ira/asco/miedo/alegría/neutro/tristeza/sorpresa."
- ai_nsfw_detection_enabled: "Activar el módulo NSFW."
- ai_nsfw_inference_service_api_endpoint: "URL donde se ejecuta la API para el módulo NSFW"
- ai_nsfw_inference_service_api_key: "Clave API para la API NSFW"
- ai_nsfw_flag_automatically: "Denunciar automáticamente las publicaciones NSFW que superen los umbrales configurados."
- ai_nsfw_flag_threshold_general: "Umbral general para que una imagen se considere NSFW."
- ai_nsfw_flag_threshold_drawings: "Umbral para que un dibujo se considere NSFW."
- ai_nsfw_flag_threshold_hentai: "Umbral para que una imagen clasificada como hentai sea considerada NSFW."
- ai_nsfw_flag_threshold_porn: "Umbral para que una imagen clasificada como porno sea considerada NSFW."
- ai_nsfw_flag_threshold_sexy: "Umbral para que una imagen clasificada como sexy se considere NSFW."
- ai_nsfw_models: "Modelos que se utilizarán para la inferencia NSFW."
- ai_helper_enabled: "Activar el ayudante de IA."
- composer_ai_helper_allowed_groups: "Los usuarios de estos grupos verán el botón del asistente de IA en el compositor."
- ai_helper_allowed_in_pm: "Activar el asistente de IA del compositor en los MP."
- ai_helper_model: "Modelo que se utilizará para el asistente de IA."
- ai_helper_custom_prompts_allowed_groups: "Los usuarios de estos grupos verán la opción de aviso personalizado en el ayudante de IA."
- ai_helper_automatic_chat_thread_title_delay: "Retraso en minutos antes de que el ayudante de la IA establezca automáticamente el título del hilo del chat."
- ai_helper_automatic_chat_thread_title: "Establecer automáticamente los títulos de los hilos del chat en función de su contenido."
- ai_helper_illustrate_post_model: "Modelo a utilizar para la función ilustrar publicación del ayudante de IA del compositor"
- ai_helper_enabled_features: "Selecciona las funciones que quieres activar en el ayudante de IA."
- post_ai_helper_allowed_groups: "Grupos de usuarios autorizados a acceder a las funciones del ayudante de IA en las publicaciones"
- ai_helper_image_caption_model: "Selecciona el modelo que se utilizará para generar pies de foto"
- ai_auto_image_caption_allowed_groups: "Los usuarios de estos grupos pueden activar el subtitulado automático de imágenes."
- ai_embeddings_selected_model: "Utiliza el modelo seleccionado para generar incrustaciones."
- ai_embeddings_generate_for_pms: "Generar incrustaciones para mensajes personales."
- ai_embeddings_semantic_related_topics_enabled: "Utilizar la Búsqueda semántica para temas relacionados."
- ai_embeddings_semantic_related_topics: "Número máximo de temas que se mostrarán en la sección de temas relacionados."
- ai_embeddings_backfill_batch_size: "Número de incrustaciones a rellenar cada 15 minutos."
- ai_embeddings_semantic_search_enabled: "Activar la búsqueda semántica a página completa."
- ai_embeddings_semantic_quick_search_enabled: "Activa la opción de búsqueda semántica en el menú emergente de búsqueda."
- ai_embeddings_semantic_related_include_closed_topics: "Incluir temas cerrados en los resultados de la búsqueda semántica"
- ai_embeddings_semantic_search_hyde_model: "Modelo utilizado para expandir palabras clave para obtener mejores resultados durante una búsqueda semántica"
- ai_embeddings_per_post_enabled: Generar incrustaciones para cada publicación
- ai_summarization_model: "Modelo que se utilizará para el resumen"
- ai_custom_summarization_allowed_groups: "Grupos autorizados a utilizar la creación de nuevos resúmenes."
- ai_pm_summarization_allowed_groups: "Grupos autorizados a crear y ver resúmenes en MPs."
- ai_summary_gists_enabled: "Genera automáticamente breves resúmenes de las últimas respuestas en los temas"
- ai_summary_gists_allowed_groups: "Grupos autorizados a ver fragmentos en la lista de temas candentes."
- ai_summary_backfill_maximum_topics_per_hour: "Número de resúmenes de temas a completar por hora."
- ai_bot_enabled: "Activar el módulo AI Bot."
- ai_bot_enable_chat_warning: "Muestra una advertencia cuando se inicia el chat de MP. Se puede anular editando la cadena de traducción: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Cuando el Bot GPT tenga acceso al MP, responderá a los miembros de estos grupos."
- ai_bot_debugging_allowed_groups: "Permitir que estos grupos vean un botón de depuración en las publicaciones que muestre la solicitud y la respuesta sin procesar de la IA."
- ai_bot_public_sharing_allowed_groups: "Permitir que estos grupos compartan mensajes personales de IA con el público a través de un enlace único disponible públicamente. Nota: si tu sitio requiere inicio de sesión, los mensajes compartidos también requerirán inicio de sesión."
- ai_bot_add_to_header: "Mostrar un botón en el encabezado para iniciar un MP con un bot de IA"
- ai_bot_github_access_token: "Token de acceso a GitHub para utilizarlo con las herramientas de IA de GitHub (necesario para la compatibilidad con búsquedas)"
- ai_stability_api_key: "Clave API para la API de stability.ai"
- ai_stability_engine: "Motor de generación de imágenes que se utilizará para la API stability.ai"
- ai_stability_api_url: "URL para la API de stability.ai"
- ai_google_custom_search_api_key: "Clave API para la API de búsqueda personalizada de Google, consulta: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX para la API de búsqueda personalizada de Google"
- reviewables:
- reasons:
- flagged_by_toxicity: El plugin de IA lo denunció tras clasificarlo como tóxico.
- flagged_by_nsfw: El plugin de IA denunció esto después de clasificar al menos una de las imágenes adjuntas como NSFW.
- reports:
- overall_sentiment:
- title: "Sentimiento general"
- description: 'El gráfico compara el número de publicaciones clasificadas como positivas o negativas. Se calculan cuando las puntuaciones positivas o negativas superan la puntuación umbral establecida. Esto significa que no se muestran las publicaciones neutrales. También se excluyen los mensajes personales (MP). Clasificados con «cardiffnlp/twitter-roberta-base-sentiment-latest»'
- xaxis: "Positivas(%)"
- yaxis: "Fecha"
- emotion_admiration:
- title: "\U0001F929 Admiración"
- description: "Publicaciones clasificadas con la emoción Admiración mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_amusement:
- title: "\U0001F604 Diversión"
- description: "Publicaciones clasificadas con la emoción Diversión mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_anger:
- title: "\U0001F620 Enfado"
- description: "Publicaciones clasificadas con la emoción Ira mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_annoyance:
- title: "\U0001F612 Molestia"
- description: "Publicaciones clasificadas con la emoción Molestia mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_approval:
- title: "\U0001F44D Aprobación"
- description: "Mensajes clasificados con la emoción Aprobación mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_caring:
- title: "\U0001F917 Cuidado"
- description: "Publicaciones clasificadas con la emoción Cuidado mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_confusion:
- title: "\U0001F615 Confusión"
- description: "Publicaciones clasificadas con la emoción Confusión mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_curiosity:
- title: "\U0001F914 Curiosidad"
- description: "Publicaciones clasificadas con la emoción Curiosidad mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_desire:
- title: "\U0001F60D Deseo"
- description: "Publicaciones clasificadas con la emoción Deseo mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_disappointment:
- title: "\U0001F61E Decepción"
- description: "Publicaciones clasificadas con la emoción Decepción mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_disapproval:
- title: "\U0001F44E Desaprobación"
- description: "Publicaciones clasificadas con la emoción Desaprobación mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_disgust:
- title: "\U0001F922 Asco"
- description: "Publicaciones clasificadas con la emoción Asco mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_embarrassment:
- title: "\U0001F633 Vergüenza"
- description: "Publicaciones clasificadas con la emoción Vergüenza mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_excitement:
- title: "\U0001F92A Excitación"
- description: "Publicaciones clasificadas con la emoción Excitación mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_fear:
- title: "\U0001F628 Miedo"
- description: "Publicaciones clasificadas con la emoción Miedo mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_gratitude:
- title: "\U0001F64F Gratitud"
- description: "Publicaciones clasificadas con la emoción Gratitud mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_grief:
- title: "\U0001F622 Duelo"
- description: "Publicaciones clasificadas con la emoción Duelo mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_joy:
- title: "\U0001F60A Alegría"
- description: "Publicaciones clasificadas con la emoción Alegría mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_love:
- title: '❤️ Amor'
- description: "Publicaciones clasificadas con la emoción Amor mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_nervousness:
- title: "\U0001F630 Nerviosismo"
- description: "Publicaciones clasificadas con la emoción Nerviosismo mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_neutral:
- title: "\U0001F610 Neutro"
- description: "Publicaciones clasificadas con la emoción Neutro mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_optimism:
- title: "\U0001F31F Optimismo"
- description: "Publicaciones clasificadas con la emoción Optimismo mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_pride:
- title: "\U0001F981 Orgullo"
- description: "Publicaciones clasificadas con la emoción Orgullo mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_realization:
- title: "\U0001F4A1 Realización"
- description: "Mensajes clasificados con la emoción Realización mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_relief:
- title: "\U0001F60C Alivio"
- description: "Publicaciones clasificadas con la emoción Alivio mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_remorse:
- title: "\U0001F614 Remordimiento"
- description: "Publicaciones clasificadas con la emoción Remordimiento mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_sadness:
- title: "\U0001F62D Tristeza"
- description: "Publicaciones clasificadas con la emoción Tristeza mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- emotion_surprise:
- title: "\U0001F632 Sorpresa"
- description: "Publicaciones clasificadas con la emoción Sorpresa mediante IA, utilizando el modelo 'SamLowe/roberta-base-go_emotions'."
- discourse_ai:
- ai_artifact:
- view_source: "Ver fuente"
- view_changes: "Ver cambios"
- unknown_model: "Modelo de IA desconocido"
- tools:
- custom_name: "%{name} (personalizado)"
- presets:
- browse_web_jina:
- name: "Navegar por la web (jina.ai)"
- exchange_rate:
- name: "Tipo de cambio"
- stock_quote:
- name: "Cotización de acciones (AlphaVantage)"
- image_generation:
- name: "Generador de imágenes de flujo (Together.ai)"
- empty_tool:
- name: "Empieza desde cero..."
- ai_helper:
- errors:
- completion_request_failed: "Algo ha ido mal al intentar proporcionar sugerencias. Inténtalo de nuevo."
- prompts:
- translate: Traducir a %{language}
- generate_titles: Sugerir títulos de temas
- proofread: Corregir el texto
- markdown_table: Generar tabla Markdown
- custom_prompt: "Aviso personalizado"
- explain: "Explicar"
- illustrate_post: "Ilustrar publicación"
- replace_dates: "Fechas inteligentes"
- painter:
- attribution:
- stable_diffusion_xl: "Imagen de Stable Diffusion XL"
- dall_e_3: "Imagen de DALL-E 3"
- image_caption:
- attribution: "Subtitulado por IA"
- share_ai:
- read_more: "Leer la transcripción completa"
- onebox_title: "Conversación de IA con %{llm_name}"
- formatted_excerpt: "Conversación de IA con %{llm_name}:\n %{excerpt}"
- title: "%{title} - Conversación IA - %{site_name}"
- errors:
- not_allowed: "No tienes permiso para compartir este tema."
- other_people_in_pm: "Los mensajes personales con otros humanos no se pueden compartir públicamente"
- other_content_in_pm: "Los mensajes personales que contengan mensajes de otras personas no pueden compartirse públicamente"
- failed_to_share: "No se pudo compartir la conversación"
- conversation_deleted: "La conversación compartida se eliminó correctamente"
- spam_detection:
- flag_reason: "Marcado como correo no deseado por Discourse AI"
- silence_reason: "Usuario silenciado automáticamente por Discourse AI"
- invalid_error_type: "Tipo de error no válido proporcionado"
- unexpected: "Se ha producido un error inesperado"
- bot_user_update_failed: "Error al actualizar el usuario bot de escaneo de correo no deseado"
- ai_bot:
- reply_error: "Lo sentimos, parece que nuestro sistema ha encontrado un problema inesperado al intentar responder.\n\n[details='Detalles del error']\n%{details}\n[/details]"
- default_pm_prefix: "[MP de bot de IA sin título]"
- personas:
- default_llm_required: "Se requiere el modelo LLM predeterminado antes de activar el Chat"
- cannot_delete_system_persona: "Las personas del sistema no se pueden eliminar, desactívalas en su lugar"
- cannot_edit_system_persona: "Las personas del sistema solo se pueden renombrar, no puedes editar las herramientas ni el aviso del sistema, en su lugar deshabilita y haz una copia"
- github_helper:
- name: "Asistente de GitHub"
- description: "Bot de IA especializado en ayudar con tareas y preguntas relacionadas con GitHub"
- general:
- name: Ayudante del foro
- description: "Bot de IA de propósito general capaz de realizar diversas tareas"
- artist:
- name: Artista
- description: "AI Bot especializado en generar imágenes"
- sql_helper:
- name: Ayudante de SQL
- description: "Bot de IA especializado en ayudar a crear consultas SQL en esta instancia de Discourse"
- settings_explorer:
- name: Explorador de ajustes
- description: "Bot de IA especializado en ayudar a explorar los ajustes del sitio Discourse"
- creative:
- name: Creativa
- description: "Bot de IA sin integraciones externas especializado en tareas creativas"
- dall_e3:
- name: "DALL-E 3"
- description: "Bot de IA especializado en generar imágenes usando DALL-E 3"
- discourse_helper:
- name: "Asistente de Discourse"
- description: "Bot de IA especializado en ayudar con tareas relacionadas con Discourse"
- web_artifact_creator:
- name: "Creador de artefactos web"
- description: "Bot de IA especializado en crear artefactos web interactivos"
- custom_prompt:
- name: "Instruccón personalizada"
- smart_dates:
- name: "Fechas inteligentes"
- topic_not_found: "¡Resumen no disponible, tema no encontrado!"
- summarizing: "Resumiendo tema"
- searching: "Buscando: '%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "Número máximo de resultados"
- google:
- base_query:
- name: "Consulta de búsqueda básica"
- description: "Consulta base que se utilizará en la búsqueda. Ejemplos: 'site:ejemplo.com' solo incluirá resultados de ejemplo.com, before:2022-01-01 solo incluirá resultados de 2021 y anteriores. Este texto se añade a la consulta de búsqueda."
- read:
- read_private:
- name: "Leer privado"
- description: "Permitir el acceso a todos los temas a los que el usuario tiene acceso (por defecto, solo se incluyen los temas públicos)"
- search:
- search_private:
- name: "Buscar privado"
- description: "Incluir todos los temas a los que el usuario tiene acceso en los resultados de búsqueda (por defecto, solo se incluyen los temas públicos)"
- max_results:
- name: "Número máximo de resultados"
- description: "Número máximo de resultados que se incluirán en la búsqueda; si está vacío, se utilizarán las reglas por defecto y el recuento se escalará en función del modelo utilizado. El valor más alto es 100."
- base_query:
- name: "Consulta de búsqueda básica"
- description: "Consulta base a utilizar en la búsqueda. Ejemplo: «#urgente» antepondrá «#urgente» a la consulta de búsqueda y solo incluirá temas con la categoría o etiqueta urgente."
- tool_summary:
- update_artifact: "Actualizar un artefacto web"
- create_artifact: "Crear artefacto web"
- web_browser: "Navegar por Internet"
- github_search_files: "Archivos de búsqueda de GitHub"
- github_search_code: "Búsqueda de código en GitHub"
- github_file_content: "Contenido del archivo de GitHub"
- github_pull_request_diff: "Diferencia de solicitud de extracción de GitHub"
- random_picker: "Selector aleatorio"
- categories: "Lista de categorías"
- search: "Buscar"
- tags: "Listar etiquetas"
- time: "Hora"
- summarize: "Resumir"
- image: "Generar imagen"
- google: "Buscar en Google"
- read: "Leer tema"
- setting_context: "Buscar contexto de ajuste del sitio"
- schema: "Buscar esquema de base de datos"
- search_settings: "Buscando los ajustes del sitio"
- dall_e: "Generar imagen"
- search_meta_discourse: "Buscar en Discourse Meta"
- javascript_evaluator: "Evaluar JavaScript"
- tool_help:
- update_artifact: "Actualizar un artefacto web usando el bot de IA"
- create_artifact: "Crear un artefacto web usando el bot de IA"
- web_browser: "Navegar por la página web utilizando el bot de IA"
- github_search_code: "Buscar código en un repositorio de GitHub"
- github_search_files: "Buscar archivos en un repositorio de GitHub"
- github_file_content: "Recuperar el contenido de los archivos de un repositorio de GitHub"
- github_pull_request_diff: "Recuperar una diferencia de solicitud de extracción de GitHub"
- random_picker: "Elige un número aleatorio o un elemento aleatorio de una lista"
- categories: "Listar todas las categorías visibles públicamente en el foro"
- search: "Buscar todos los temas públicos en el foro."
- tags: "Listar todas las etiquetas en el foro"
- time: "Encontrar hora en varias zonas horarias"
- summary: "Resumir un tema"
- image: "Generar imagen usando Stable Diffusion"
- google: "Buscar en Google una consulta"
- read: "Leer tema público en el foro."
- setting_context: "Buscar contexto de ajuste del sitio"
- schema: "Buscar esquema de base de datos"
- search_settings: "Buscar ajustes del sitio"
- dall_e: "Generar imagen usando DALL-E 3"
- search_meta_discourse: "Buscar en Discourse Meta"
- javascript_evaluator: "Evaluar JavaScript"
- tool_description:
- update_artifact: "Se actualizó un artefacto web utilizando el bot de IA"
- web_browser: "Leyendo %{url}"
- github_search_files: "Se buscó «%{keywords}» en %{repo}/%{branch}"
- github_search_code: "Se buscó «%{query}» en %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "Contenido recuperado de %{file_paths} de %{repo_name}@%{branch}"
- random_picker: "Escogiendo entre %{options}, escogido: %{result}"
- read: "Leyendo: %{title}"
- time: "La hora en %{timezone} es %{time}"
- summarize: "Resumido %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "Se encontró %{count} categoría"
- other: "Se encontraron %{count} categorías"
- tags:
- one: "Se encontró %{count} etiqueta"
- other: "Se encontraron %{count} etiquetas"
- search:
- one: "Se encontró %{count} resultado para '%{query}'"
- other: "Se han encontrado %{count} resultados para '%{query}'"
- search_meta_discourse:
- one: "Se encontró %{count} resultado para '%{query}'"
- other: "Se han encontrado %{count} resultados para '%{query}'"
- google:
- one: "Se encontró %{count} resultado para '%{query}'"
- other: "Se han encontrado %{count} resultados para '%{query}'"
- setting_context: "Leyendo contexto parar: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "Se encontró %{count} resultado para '%{query}'"
- other: "Se han encontrado %{count} resultados para '%{query}'"
- summarization:
- configuration_hint:
- one: "Configura primero el ajuste `%{setting}`."
- other: "Configura primero estos ajustes: %{settings}"
- chat:
- no_targets: "No hubo mensajes durante el periodo seleccionado."
- sentiment:
- reports:
- overall_sentiment: "Sentimiento general (positivo - negativo)"
- post_emotion:
- sadness: "Tristeza \U0001F622"
- surprise: "Sorpresa \U0001F631"
- neutral: "Neutro \U0001F610"
- fear: "Miedo \U0001F628"
- anger: "Enfado \U0001F621"
- joy: "Alegría \U0001F600"
- disgust: "Asco \U0001F922"
- sentiment_analysis:
- positive: "Positivo"
- negative: "Negativo"
- neutral: "Neutro"
- llm:
- configuration:
- disable_module_first: "Tienes que desactivar primero %{setting}."
- set_llm_first: "Establecer primero %{setting}"
- model_unreachable: "No hemos podido obtener una respuesta de este modelo. Comprueba primero los ajustes."
- invalid_seeded_model: "No puedes utilizar este modelo con esta característica"
- must_select_model: "Primero debes seleccionar un LLM"
- endpoints:
- not_configured: "%{display_name} (no configurado)"
- configuration_hint:
- one: "Asegúrate de que se ha configurado el ajuste «%{settings}»."
- other: "Asegúrate de que se han configurado estos ajustes: «%{settings}»."
- delete_failed:
- one: "No hemos podido eliminar este modelo porque %{settings} lo está usando. Actualiza el ajuse e inténtalo de nuevo."
- other: "No hemos podido eliminar este modelo porque %{settings} lo está usando. Actualiza los ajustes e inténtalo de nuevo."
- cannot_edit_builtin: "No puedes editar un modelo integrado."
- embeddings:
- delete_failed: "Este modelo está actualmente en uso. Actualiza primero el «modelo seleccionado de incrustaciones de IA»."
- cannot_edit_builtin: "No puedes editar un modelo integrado."
- configuration:
- disable_embeddings: "Tienes que desactivar primero «incrustaciones de ia activadas»."
- choose_model: "Establece primero «modelo seleccionado de incrustaciones de ia»."
- llm_models:
- missing_provider_param: "%{param} no puede estar en blanco"
- bedrock_invalid_url: "Rellena todos los campos para utilizar este modelo."
- ai_staff_action_logger:
- updated: "actualizado"
- removed: "eliminado"
- errors:
- quota_exceeded: "Has superado la cuota para este modelo. Inténtalo de nuevo en %{relative_time}."
- quota_required: "Debes especificar los tokens o usos máximos para este modelo"
- no_query_specified: El parámetro de consulta es obligatorio, especifícalo.
- no_user_for_persona: La persona especificada no tiene ningún usuario asociado.
- persona_not_found: La persona especificada no existe. Comprueba los parámetros persona_name o persona_id.
- no_user_specified: El nombre de usuario o user_unique_id es obligatorio, por favor, especifícalo.
- user_not_found: El usuario especificado no existe. Comprueba el parámetro username.
- persona_disabled: La persona especificada está desactivada. Comprueba los parámetros persona_name o persona_id.
- no_default_llm: La persona debe tener un default_llm definido.
- user_not_allowed: El usuario no está autorizado a participar en el tema.
- prompt_message_length: El mensaje %{idx} supera el límite de 1000 caracteres.
diff --git a/config/locales/server.et.yml b/config/locales/server.et.yml
deleted file mode 100644
index e4b16e4d..00000000
--- a/config/locales/server.et.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-et:
- reports:
- overall_sentiment:
- yaxis: "Date"
- emotion_neutral:
- title: "\U0001F610 Neutraalne"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Otsi"
- time: "Aeg"
- sentiment:
- reports:
- post_emotion:
- neutral: "Neutraalne \U0001F610"
- sentiment_analysis:
- neutral: "Neutraalne"
diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml
deleted file mode 100644
index 1cc005fa..00000000
--- a/config/locales/server.fa_IR.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-fa_IR:
- site_settings:
- discourse_ai_enabled: "افزونه discourse AI را فعال کنید."
- reports:
- overall_sentiment:
- yaxis: "تاریخ"
- discourse_ai:
- share_ai:
- onebox_title: "گفتگوی هوشمصنوعی با %{llm_name}"
- formatted_excerpt: "گفتگوی هوشمصنوعی با %{llm_name}:\n %{excerpt}"
- title: "%{title} - گفتگوی هوشمصنوعی - %{site_name}"
- ai_bot:
- personas:
- dall_e3:
- name: "DALL-E 3"
- tool_summary:
- categories: "فهرست دستهبندیها"
- search: "جستجو"
- tags: "فهرست برچسبها"
- time: "زمان"
- summarize: "خلاصه کنید"
- image: "تولید تصویر"
- google: "جستجو در گوگل"
- dall_e: "تولید تصویر"
- tool_description:
- summarize: "خلاصه شده %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- ai_staff_action_logger:
- updated: "به روز شده"
- removed: "پاک شد"
diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml
deleted file mode 100644
index 82e38546..00000000
--- a/config/locales/server.fi.yml
+++ /dev/null
@@ -1,445 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-fi:
- discourse_automation:
- ai:
- flag_types:
- review: "Lisää viesti tarkastusjonoon"
- spam: "Liputa roskapostiksi ja piilota viesti"
- spam_silence: "Liputa roskapostiksi, piilota viesti ja hiljennä käyttäjä"
- scriptables:
- llm_triage:
- title: Luokittele viestit tekoälyllä
- description: "Luokittele viestit suurella kielimallilla"
- flagged_post: |
-
Mallin vastaus::
-
%%LLM_RESPONSE%%
- Säännön %%AUTOMATION_NAME%% käynnistämä.
- llm_report:
- title: Säännöllinen raportti tekoälyllä
- description: "Säännöllinen raportti laajan kielimallin perusteella"
- site_settings:
- discourse_ai_enabled: "Ota Discoursen tekoälylisäosa käyttöön."
- ai_toxicity_enabled: "Ota toksisuusmoduuli käyttöön."
- ai_toxicity_inference_service_api_endpoint: "URL-osoite, jossa toksisuusmoduulin API on käynnissä"
- ai_toxicity_inference_service_api_key: "Toksisuus-APIn API-avain"
- ai_toxicity_inference_service_api_model: "Päätelmien tekemiseen käytettävä malli. Monikielinen malli toimii italian, ranskan, venäjän, portugalin, espanjan ja turkin kielillä."
- ai_toxicity_flag_automatically: "Liputa automaattisesti viestit ja chat-viestit, jotka ylittävät määritetyt rajat."
- ai_toxicity_flag_threshold_toxicity: "Toksisuus: töykeä, epäkunnioittava tai kohtuuton kommentti, joka melko todennäköisesti saa sinut poistumaan keskustelusta tai jättämään näkökulmasi jakamatta"
- ai_toxicity_flag_threshold_severe_toxicity: "Vakava toksisuus: erittäin vihamielinen, aggressiivinen tai epäkunnioittava kommentti, joka hyvin todennäköisesti saa sinut poistumaan keskustelusta tai jättämään näkökulmasi jakamatta"
- ai_toxicity_flag_threshold_obscene: "Säädytön"
- ai_toxicity_flag_threshold_identity_attack: "Identiteettihyökkäys"
- ai_toxicity_flag_threshold_insult: "Loukkaus"
- ai_toxicity_flag_threshold_threat: "Uhkaus"
- ai_toxicity_flag_threshold_sexual_explicit: "Seksuaalinen"
- ai_toxicity_groups_bypass: "Toksisuusmoduuli ei luokittele näiden ryhmien käyttäjien viestejä."
- ai_sentiment_enabled: "Ota tunnemoduuli käyttöön."
- ai_sentiment_inference_service_api_endpoint: "URL-osoite, jossa tunnemoduulin API on käynnissä"
- ai_sentiment_inference_service_api_key: "Tunne-APIn API-avain"
- ai_sentiment_models: "Päätelmien tekemiseen käytettävät mallit. Tunne luokittelee viestin positiivinen/neutraali/negatiivinen-skaalalla. Tunne luokittelee viha/inho/pelko/ilo/neutraali/suru/yllätys-skaalalla."
- ai_nsfw_detection_enabled: "Ota NSFW-moduuli käyttöön."
- ai_nsfw_inference_service_api_endpoint: "URL-osoite, jossa NSFW-moduulin API on käynnissä"
- ai_nsfw_inference_service_api_key: "NSFW-APIn API-avain"
- ai_nsfw_flag_automatically: "Liputa automaattisesti NSFW-viestit, jotka ylittävät määritetyt rajat."
- ai_nsfw_flag_threshold_general: "Yleinen raja, jolloin kuvaa pidetään NSFW:nä."
- ai_nsfw_flag_threshold_drawings: "Raja, jolloin piirrosta pidetään NSFW:nä."
- ai_nsfw_flag_threshold_hentai: "Raja, jolloin hentaiksi luokiteltua kuvaa pidetään NSFW:nä."
- ai_nsfw_flag_threshold_porn: "Raja, jolloin pornoksi luokiteltua kuvaa pidetään NSFW:nä."
- ai_nsfw_flag_threshold_sexy: "Raja, jolloin seksikkääksi luokiteltua kuvaa pidetään NSFW:nä."
- ai_nsfw_models: "NSFW-päätelmiin käytettävät mallit."
- ai_helper_enabled: "Ota tekoälyapuri käyttöön."
- composer_ai_helper_allowed_groups: "Näiden ryhmien käyttäjät näkevät tekoälyavustajan painikkeen kirjoitustoiminnossa."
- ai_helper_allowed_in_pm: "Ota kirjoitustoiminnon tekoälyavustaja käyttöön yksityisviesteissä."
- ai_helper_model: "Tekoälyavustajassa käytettävä malli."
- ai_helper_custom_prompts_allowed_groups: "Näiden ryhmien käyttäjät näkevät mukautetun kehotteen vaihtoehdon tekoälyapuohjelmassa."
- ai_helper_automatic_chat_thread_title_delay: "Viive minuuteissa ennen kuin tekoälyapuohjelma asettaa chat-ketjun otsikon automaattisesti."
- ai_helper_automatic_chat_thread_title: "Aseta chat-ketjujen otsikot automaattisesti ketjun sisällön perusteella."
- ai_helper_illustrate_post_model: "Malli, jota käytetään tekstieditorin tekoälyapuohjelman viestin havainnollistamisominaisuudessa"
- ai_helper_enabled_features: "Valitse ominaisuudet, jotka otetaan käyttöön tekoälyavustajassa."
- post_ai_helper_allowed_groups: "Käyttäjäryhmät, joilla on oikeus käyttää tekoälyavustajan ominaisuuksia viesteissä"
- ai_helper_image_caption_model: "Valitse malli, jota käytetään kuvien kuvatekstien luomiseen"
- ai_auto_image_caption_allowed_groups: "Näiden ryhmien käyttäjät voivat ottaa automaattisen kuvatekstityksen käyttöön tai poistaa sen käytöstä."
- ai_embeddings_selected_model: "Käytä valittua mallia upotusten luomiseen."
- ai_embeddings_generate_for_pms: "Luo upotuksia yksityisviesteille."
- ai_embeddings_semantic_related_topics_enabled: "Käytä semanttista hakua aiheeseen liittyviä ketjuja varten."
- ai_embeddings_semantic_related_topics: "Aiheeseen liittyvien ketjujen osiossa näytettävien ketjujen enimmäismäärä."
- ai_embeddings_backfill_batch_size: "15 minuutin välein täydennettävien upotusten määrä."
- ai_embeddings_semantic_search_enabled: "Ota koko sivun semanttinen haku käyttöön."
- ai_embeddings_semantic_quick_search_enabled: "Ota semanttinen haku käyttöön hakuvalikon ponnahdusikkunassa."
- ai_embeddings_semantic_related_include_closed_topics: "Sisällytä suljetut ketjut semanttisiin hakutuloksiin"
- ai_embeddings_semantic_search_hyde_model: "Malli, jota käytetään avainsanojen laajentamiseen parempien tulosten saamiseksi semanttisen haun aikana"
- ai_embeddings_per_post_enabled: Luo upotukset jokaiselle viestille
- ai_summarization_model: "Yhteenvetoon käytettävä malli"
- ai_custom_summarization_allowed_groups: "Ryhmät, jotka voivat luoda uusia yhteenvetoja."
- ai_pm_summarization_allowed_groups: "Ryhmät voivat luoda ja tarkastella yhteenvetoja yksityisviesteissä."
- ai_summary_gists_enabled: "Luo lyhyitä yhteenvetoja ketjujen uusimmista vastauksista automaattisesti"
- ai_summary_gists_allowed_groups: "Ryhmät voivat nähdä yhteenvetoja kuumien ketjujen luettelossa."
- ai_summary_backfill_maximum_topics_per_hour: "Täydennettävien ketjujen yhteenvetojen määrä tunnissa."
- ai_bot_enabled: "Ota tekoälybottimoduuli käyttöön."
- ai_bot_enable_chat_warning: "Näytä varoitus, kun yksityisviesti-chat aloitetaan. Voidaan ohittaa muokkaamalla käännösmerkkijonoa: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Kun GPT-botilla on yksityisviestin käyttöoikeus, se vastaa näiden ryhmien jäsenille."
- ai_bot_debugging_allowed_groups: "Salli näiden ryhmien nähdä viesteissä virheenkorjauspainike, joka näyttää raa'an tekoälypyynnön ja vastauksen"
- ai_bot_public_sharing_allowed_groups: "Salli näiden ryhmien jakaa tekoälyn yksityisviestejä yleisölle ainutlaatuisen julkisesti käytettävissä olevan linkin kautta. Huomaa: jos sivustosi edellyttää kirjautumista, myös jaot edellyttävät kirjautumista."
- ai_bot_add_to_header: "Näytä painike yläpalkissa yksityiskeskustelun aloittamiseksi tekoälybotin kanssa"
- ai_bot_github_access_token: "GitHubin käyttötunnus käytettäväksi GitHubin tekoälytyökalujen kanssa (vaaditaan haun tukea varten)"
- ai_stability_api_key: "stability.ai-APIn API-avain"
- ai_stability_engine: "stability.ai-APIssa käytettävä kuvanluontimoduuli"
- ai_stability_api_url: "stability.ai-APIn URL"
- ai_google_custom_search_api_key: "Google Custom Search APIn API-avain, katso: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "Google Custom Search APIn CX"
- reviewables:
- reasons:
- flagged_by_toxicity: Tekoälylisäosa liputti tämän luokiteltuaan sen toksiseksi.
- flagged_by_nsfw: Tekoälylisäosa liputti tämän luokiteltuaan ainakin yhden liitetyistä kuvista NSFW:ksi.
- reports:
- overall_sentiment:
- title: "Yleinen tunne"
- description: 'Kaaviossa verrataan positiivisiksi tai negatiivisiksi luokiteltujen viestien määrää. Nämä lasketaan, kun positiiviset tai negatiiviset pisteet ylittävät asetetun kynnysarvon. Tämä tarkoittaa, että neutraaleja viestejä ei näytetä. Myöskään yksityisviestejä ei lasketa. Luokiteltu "cardiffnlp/twitter-roberta-base-sentiment-latest"-mallilla.'
- xaxis: "Positiivinen (%)"
- yaxis: "Päivämäärä"
- emotion_admiration:
- title: "\U0001F929 Ihailu"
- description: "Viestit, jotka on luokiteltu ihailun tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_amusement:
- title: "\U0001F604 Huvittuneisuus"
- description: "Viestit, jotka on luokiteltu huvittuneisuuden tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_anger:
- title: "\U0001F620 Viha"
- description: "Viestit, jotka on luokiteltu vihan tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_annoyance:
- title: "\U0001F612 Ärsytys"
- description: "Viestit, jotka on luokiteltu ärsytyksen tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_approval:
- title: "\U0001F44D Hyväksyntä"
- description: "Viestit, jotka on luokiteltu hyväksynnän tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_caring:
- title: "\U0001F917 Välittäminen"
- description: "Viestit, jotka on luokiteltu välittämisen tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_confusion:
- title: "\U0001F615 Hämmennys"
- description: "Viestit, jotka on luokiteltu hämmennyksen tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_curiosity:
- title: "\U0001F914 Uteliaisuus"
- description: "Viestit, jotka on luokiteltu uteliaisuuden tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_desire:
- title: "\U0001F60D Halu"
- description: "Viestit, jotka on luokiteltu halun tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_disappointment:
- title: "\U0001F61E Pettymys"
- description: "Viestit, jotka on luokiteltu pettymyksen tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_disapproval:
- title: "\U0001F44E Paheksunta"
- description: "Viestit, jotka on luokiteltu paheksunnan tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_disgust:
- title: "\U0001F922 Inho"
- description: "Viestit, jotka on luokiteltu inhon tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_embarrassment:
- title: "\U0001F633 Häpeä"
- description: "Viestit, jotka on luokiteltu häpeän tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_excitement:
- title: "\U0001F92A Innostus"
- description: "Viestit, jotka on luokiteltu innostuksen tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_fear:
- title: "\U0001F628 Pelko"
- description: "Viestit, jotka on luokiteltu pelon tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_gratitude:
- title: "\U0001F64F Kiitollisuus"
- description: "Viestit, jotka on luokiteltu kiitollisuuden tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_grief:
- title: "\U0001F622 Murheellisuus"
- description: "Viestit, jotka on luokiteltu murheellisuuden tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_joy:
- title: "\U0001F60A Ilo"
- description: "Viestit, jotka on luokiteltu ilon tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_love:
- title: '❤️ Rakkaus'
- description: "Viestit, jotka on luokiteltu rakkauden tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_nervousness:
- title: "\U0001F630 Hermostuneisuus"
- description: "Viestit, jotka on luokiteltu hermostuneisuuden tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_neutral:
- title: "\U0001F610 Neutraali"
- description: "Viestit, jotka on luokiteltu neutraalilla tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_optimism:
- title: "\U0001F31F Optimismi"
- description: "Viestit, jotka on luokiteltu optimismin tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_pride:
- title: "\U0001F981 Ylpeys"
- description: "Viestit, jotka on luokiteltu ylpeyden tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_realization:
- title: "\U0001F4A1 Oivallus"
- description: "Viestit, jotka on luokiteltu oivalluksen tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_relief:
- title: "\U0001F60C Helpotus"
- description: "Viestit, jotka on luokiteltu helpotuksen tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_remorse:
- title: "\U0001F614 Katumus"
- description: "Viestit, jotka on luokiteltu katumuksen tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_sadness:
- title: "\U0001F62D Suru"
- description: "Viestit, jotka on luokiteltu surullisuuden tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- emotion_surprise:
- title: "\U0001F632 Yllätys"
- description: "Viestit, jotka on luokiteltu yllätyksen tunteella tekoälyllä mallilla \"SamLowe/roberta-base-go_emotions\"."
- discourse_ai:
- ai_artifact:
- view_source: "Näytä lähde"
- view_changes: "Näytä muutokset"
- unknown_model: "Tuntematon tekoälymalli"
- tools:
- custom_name: "%{name} (mukautettu)"
- presets:
- browse_web_jina:
- name: "Selaa verkkoa (jina.ai)"
- exchange_rate:
- name: "Vaihtokurssi"
- stock_quote:
- name: "Osakekurssi (AlphaVantage)"
- image_generation:
- name: "Flux-kuvageneraattori (Together.ai)"
- empty_tool:
- name: "Aloita tyhjästä..."
- ai_helper:
- errors:
- completion_request_failed: "Jokin meni vikaan yritettäessä antaa ehdotuksia. Yritä uudelleen."
- prompts:
- translate: Käännä kielelle %{language}
- generate_titles: Ehdota ketjujen otsikoita
- proofread: Oikolue teksti
- markdown_table: Luo markdown-taulukko
- custom_prompt: "Mukautettu kehote"
- explain: "Selitä"
- illustrate_post: "Havainnollista viestiä"
- replace_dates: "Älykkäät päivämäärät"
- painter:
- attribution:
- stable_diffusion_xl: "Kuvan tarjoaa Stable Diffusion XL"
- dall_e_3: "Kuvan tarjoaa DALL-E 3"
- image_caption:
- attribution: "Tekoälyn laatima kuvateksti"
- share_ai:
- read_more: "Lue koko transkriptio"
- onebox_title: "Tekoälykeskustelu LLM:n %{llm_name} kanssa"
- formatted_excerpt: "Tekoälykeskustelu LLM:n %{llm_name} kanssa:\n %{excerpt}"
- title: "%{title} – Tekoälykeskustelu – %{site_name}"
- errors:
- not_allowed: "Sinulla ei ole oikeutta jakaa tätä ketjua"
- other_people_in_pm: "Yksityisviestejä muiden ihmisten kanssa ei voi jakaa julkisesti"
- other_content_in_pm: "Yksityisviestejä, jotka sisältävät muiden ihmisten viestejä, ei voi jakaa julkisesti"
- failed_to_share: "Keskustelun jakaminen epäonnistui"
- conversation_deleted: "Keskustelun jaon poistaminen onnistui"
- spam_detection:
- flag_reason: "Discourse AI liputti roskapostiksi"
- silence_reason: "Discourse AI hiljensi käyttäjän automaattisesti"
- invalid_error_type: "Virheellinen virhetyyppi annettu"
- unexpected: "Tapahtui odottamaton virhe"
- bot_user_update_failed: "Roskapostin skannausbottikäyttäjän päivittäminen epäonnistui"
- ai_bot:
- reply_error: "Näyttää siltä, että järjestelmässä tapahtui odottamaton ongelma, kun se yritti vastata.\n\n[details='Error details']\n%{details}\n[/details]"
- default_pm_prefix: "[Nimetön tekoälybotin yksityisviesti]"
- personas:
- default_llm_required: "Oletus-LLM-malli vaaditaan ennen chatin käyttöönottoa"
- cannot_delete_system_persona: "Järjestelmäpersoonia ei voi poistaa, poista se sen sijaan käytöstä"
- cannot_edit_system_persona: "Järjestelmäpersoonia voi vain nimetä uudelleen, et saa muokata työkaluja tai järjestelmäkehotetta, sen sijaan voit poistaa ne käytöstä ja tehdä kopion"
- github_helper:
- name: "GitHub-apulainen"
- description: "Tekoälybotti, joka on erikoistunut avustamaan GitHubiin liittyvissä tehtävissä ja kysymyksissä"
- general:
- name: Foorumin apulainen
- description: "Yleiskäyttöinen tekoälybotti, joka pystyy suorittamaan erilaisia tehtäviä"
- artist:
- name: Taiteilija
- description: "Kuvien luomiseen erikoistunut tekoälybotti"
- sql_helper:
- name: SQL-apuohjelma
- description: "Tekoälybotti, joka on erikoistunut SQL-kyselyiden laatimiseen tässä Discourse-esiintymässä"
- settings_explorer:
- name: Asetusapuri
- description: "Tekoälybotti, joka on erikoistunut auttamaan Discoursen sivustoasetuksiin tutustumisessa"
- creative:
- name: Luova
- description: "Tekoälybotti ilman ulkoisia integraatioita, joka on erikoistuneet luoviin tehtäviin"
- dall_e3:
- name: "DALL-E 3"
- description: "Kuvien luomiseen DALL-E 3:lla erikoistunut tekoälybotti"
- discourse_helper:
- name: "Discourse-apulainen"
- description: "Tekoälybotti, joka on erikoistunut auttamaan Discourseen liittyvissä tehtävissä"
- web_artifact_creator:
- name: "Verkkoartefaktien luontityökalu"
- description: "Interaktiivisten verkkoartefaktien luomiseen erikoistunut tekoälybotti"
- custom_prompt:
- name: "Mukautettu kehote"
- smart_dates:
- name: "Älykkäät päivämäärät"
- topic_not_found: "Yhteenveto ei ole saatavilla, ketjua ei löydy!"
- summarizing: "Laaditaan yhteenvetoa ketjusta"
- searching: "Haetaan: \"%{query}\""
- tool_options:
- researcher:
- max_results:
- name: "Tulosten enimmäismäärä"
- google:
- base_query:
- name: "Perushakukysely"
- description: "Haussa käytettävä peruskysely. Esimerkkejä: \"site:example.com\" sisältää vain tulokset osoitteesta example.com, before:2022-01-01 sisältää vain tulokset vuodelta 2021 ja sitä ennen. Tämä teksti lisätään hakukyselyn alkuun."
- read:
- read_private:
- name: "Lue yksityisesti"
- description: "Salli pääsy kaikkiin ketjuihin, joihin käyttäjällä on pääsy (sisältää oletuksena vain julkiset ketjut)"
- search:
- search_private:
- name: "Hae yksityisesti"
- description: "Sisällytä hakutuloksiin kaikki ketjut, joihin käyttäjällä on pääsy (vain julkiset ketjut sisällytetään oletuksena)"
- max_results:
- name: "Tulosten enimmäismäärä"
- description: "Hakuun sisällytettävien tulosten enimmäismäärä – jos tämä on tyhjä, oletussääntöjä käytetään ja määrää skaalataan käytettävän mallin mukaan. Korkein arvo on 100."
- base_query:
- name: "Perushakukysely"
- description: "Peruskysely, jota käytetään haussa. Esimerkki: \"#kiireellinen\" lisää hakukyselyn alkuun \"#kiireellinen\" ja sisältää vain ketjut, joissa on kiireellinen alue tai tunniste."
- tool_summary:
- update_artifact: "Päivitä verkkoartefakti"
- create_artifact: "Luo verkkoartefakti"
- web_browser: "Selaa verkkoa"
- github_search_files: "GitHub-hakutiedostot"
- github_search_code: "GitHub-koodihaku"
- github_file_content: "GitHub-tiedostosisältö"
- github_pull_request_diff: "GitHub-vetopyyntöero"
- random_picker: "Satunnainen valitsin"
- categories: "Listaa alueet"
- search: "Haku"
- tags: "Listaa tunnisteet"
- time: "Aika"
- summarize: "Tee yhteenveto"
- image: "Luo kuva"
- google: "Hae Googlesta"
- read: "Lue ketju"
- setting_context: "Etsi sivustoasetuksen konteksti"
- schema: "Etsi tietokantaskeema"
- search_settings: "Haetaan sivustoasetuksia"
- dall_e: "Luo kuva"
- search_meta_discourse: "Haku Metasta Discoursessa"
- javascript_evaluator: "Arvioi JavaScript"
- tool_help:
- update_artifact: "Päivitä verkkoartefakti tekoälybotilla"
- create_artifact: "Luo verkkoartefakti tekoälybotilla"
- web_browser: "Selaa verkkosivua tekoälybotilla"
- github_search_code: "Etsi koodia GitHub-tietovarastosta"
- github_search_files: "Etsi tiedostoja GitHub-tietovarastosta"
- github_file_content: "Nouda tiedostojen sisältöä GitHub-tietovarastosta"
- github_pull_request_diff: "Nouda GitHubin vetopyynnön ero"
- random_picker: "Valitse satunnainen luku tai satunnainen luettelon elementti"
- categories: "Listaa kaikki foorumin julkisesti näkyvät alueet"
- search: "Hae kaikista foorumin julkisista ketjuista"
- tags: "Listaa kaikki foorumin tunnisteet"
- time: "Etsi aika eri aikavyöhykkeillä"
- summary: "Laadi yhteenveto ketjusta"
- image: "Luo kuva Stable Diffusionilla"
- google: "Hae Googlesta kyselyä"
- read: "Lue julkinen ketju foorumilla"
- setting_context: "Etsi sivustoasetuksen konteksti"
- schema: "Etsi tietokantaskeema"
- search_settings: "Hae sivustoasetuksia"
- dall_e: "Luo kuva DALL-E 3:lla"
- search_meta_discourse: "Haku Metasta Discoursessa"
- javascript_evaluator: "Arvioi JavaScript"
- tool_description:
- update_artifact: "Verkkoartefakti päivitettiin tekoälybotilla"
- web_browser: "Luetaan: %{url}"
- github_search_files: "Haettiin avainsanoilla \"%{keywords}\" tietovarastosta %{repo}/%{branch}"
- github_search_code: "Haettiin ehdolla \"%{query}\" tietovarastosta %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "Noudettiin tiedostojen %{file_paths} sisältö tietovarastosta %{repo_name}@%{branch}"
- random_picker: "Valitaan vaihtoehdoista %{options}, valittu: %{result}"
- read: "Luetaan: %{title}"
- time: "Aika aikavyöhykkeellä %{timezone} on %{time}"
- summarize: "Yhteenveto: %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "Löytyi %{count} alue"
- other: "Löytyi %{count} aluetta"
- tags:
- one: "Löytyi %{count} tunniste"
- other: "Löytyi %{count} tunnistetta"
- search:
- one: "Ehdolla \"%{query}\" löytyi %{count} tulos"
- other: "Ehdolla \"%{query}\" löytyi %{count} tulosta"
- search_meta_discourse:
- one: "Ehdolla \"%{query}\" löytyi %{count} tulos"
- other: "Ehdolla \"%{query}\" löytyi %{count} tulosta"
- google:
- one: "Ehdolla \"%{query}\" löytyi %{count} tulos"
- other: "Ehdolla \"%{query}\" löytyi %{count} tulosta"
- setting_context: "Luetaan konteksti asetukselle: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "Ehdolla \"%{query}\" löytyi %{count} tulos"
- other: "Ehdolla \"%{query}\" löytyi %{count} tulosta"
- summarization:
- configuration_hint:
- one: "Määritä ensin %{setting}-asetus."
- other: "Määritä ensin nämä asetukset: %{settings}"
- chat:
- no_targets: "Valitun ajanjakson aikana ei ollut viestejä."
- sentiment:
- reports:
- overall_sentiment: "Yleinen tunne (positiivinen–negatiivinen)"
- post_emotion:
- sadness: "Suru \U0001F622"
- surprise: "Yllätys \U0001F631"
- neutral: "Neutraali \U0001F610"
- fear: "Pelko \U0001F628"
- anger: "Viha \U0001F621"
- joy: "Ilo \U0001F600"
- disgust: "Inho \U0001F922"
- sentiment_analysis:
- positive: "Positiivinen"
- negative: "Negatiivinen"
- neutral: "Neutraali"
- llm:
- configuration:
- disable_module_first: "Sinun täytyy ensin poistaa %{setting} käytöstä."
- set_llm_first: "Aseta %{setting} ensin"
- model_unreachable: "Emme saaneet vastausta tästä mallista. Tarkista ensin asetuksesi."
- invalid_seeded_model: "Et voi käyttää tätä mallia tämän ominaisuuden kanssa"
- must_select_model: "Sinun täytyy valita suuri kielimalli ensin"
- endpoints:
- not_configured: "%{display_name} (ei määritetty)"
- configuration_hint:
- one: "Varmista, että asetus \"%{settings}\" on määritetty."
- other: "Varmista, että nämä asetukset on määritetty: %{settings}"
- delete_failed:
- one: "Tätä mallia ei voitu poistaa, koska %{settings} käyttää sitä. Päivitä asetus ja yritä uudelleen."
- other: "Tätä mallia ei voitu poistaa, koska %{settings} käyttävät sitä. Päivitä asetus ja yritä uudelleen."
- cannot_edit_builtin: "Et voi muokata sisäänrakennettua mallia."
- embeddings:
- delete_failed: "Tämä malli on tällä hetkellä käytössä. Päivitä \"ai embeddings selected model\" ensin."
- cannot_edit_builtin: "Et voi muokata sisäänrakennettua mallia."
- configuration:
- disable_embeddings: "Sinun täytyy ensin poistaa \"ai embeddings enabled\" käytöstä."
- choose_model: "Aseta \"ai embeddings selected model\" ensin."
- llm_models:
- missing_provider_param: "%{param} ei voi olla tyhjä"
- bedrock_invalid_url: "Täytä kaikki kentät, jotta voit käyttää tätä mallia."
- ai_staff_action_logger:
- updated: "päivitetty"
- removed: "poistettu"
- errors:
- quota_exceeded: "Olet ylittänyt tämän mallin kiintiön. Odota %{relative_time} ja yritä sitten uudelleen."
- quota_required: "Sinun on määritettävä tälle mallille saneiden tai käyttökertojen enimmäismäärä"
- no_query_specified: Kyselyparametri on pakollinen, määritä se.
- no_user_for_persona: Määritetyllä persoonalle ei ole siihen liitettyä käyttäjää.
- persona_not_found: Määritettyä persoonaa ei ole olemassa. Tarkista parametrit persona_name tai persona_id.
- no_user_specified: Parametri username tai user_unique_id vaaditaan, määritä se.
- user_not_found: Määritettyä käyttäjää ei ole olemassa. Tarkista username-parametri.
- persona_disabled: Määritetty persoona ei ole käytössä. Tarkista parametrit persona_name tai persona_id.
- no_default_llm: Persoonalla täytyy olla default_llm määritelty.
- user_not_allowed: Käyttäjä ei saa osallistua ketjuun.
- prompt_message_length: Viesti %{idx} ylittää 1 000 merkin rajan.
diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml
deleted file mode 100644
index 650a2465..00000000
--- a/config/locales/server.fr.yml
+++ /dev/null
@@ -1,445 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-fr:
- discourse_automation:
- ai:
- flag_types:
- review: "Ajouter une publication à la file d'attente de révision"
- spam: "Signaler comme spam et masquer la publication"
- spam_silence: "Signaler comme spam, masquer la publication et désactiver l'utilisateur"
- scriptables:
- llm_triage:
- title: Trier les publications à l'aide de l'IA
- description: "Triez les publications à l'aide d'un grand modèle linguistique"
- flagged_post: |
-
Réponse du modèle :
-
%%LLM_RESPONSE%%
- Déclenché par la règle %%AUTOMATION_NAME%%.
- llm_report:
- title: Rapport périodique utilisant l'IA
- description: "Rapport périodique basé sur un grand modèle linguistique"
- site_settings:
- discourse_ai_enabled: "Activez l'extension IA de Discourse."
- ai_toxicity_enabled: "Activez le module de toxicité."
- ai_toxicity_inference_service_api_endpoint: "Adresse URL où s'exécute l'API pour le module de toxicité"
- ai_toxicity_inference_service_api_key: "Clé API pour l'API de toxicité"
- ai_toxicity_inference_service_api_model: "Modèle à utiliser pour l'inférence. Le modèle multilingue fonctionne avec l'italien, le français, le russe, le portugais, l'espagnol et le turc."
- ai_toxicity_flag_automatically: "Signalez automatiquement les publications et les messages de conversation qui dépassent les seuils configurés."
- ai_toxicity_flag_threshold_toxicity: "Toxicité : commentaire grossier, irrespectueux ou déraisonnable susceptible de vous inciter à quitter une discussion ou à renoncer à partager votre point de vue"
- ai_toxicity_flag_threshold_severe_toxicity: "Toxicité grave : commentaire très haineux, agressif ou irrespectueux susceptible de vous inciter à quitter une discussion ou à renoncer à partager votre point de vue"
- ai_toxicity_flag_threshold_obscene: "Obscène"
- ai_toxicity_flag_threshold_identity_attack: "Attaque d'identité"
- ai_toxicity_flag_threshold_insult: "Insulte"
- ai_toxicity_flag_threshold_threat: "Menace"
- ai_toxicity_flag_threshold_sexual_explicit: "Sexualité explicite"
- ai_toxicity_groups_bypass: "Les utilisateurs de ces groupes ne verront pas leurs messages classés par le module de toxicité."
- ai_sentiment_enabled: "Activez le module de sentiment."
- ai_sentiment_inference_service_api_endpoint: "Adresse URL où s'exécute l'API pour le module de sentiment"
- ai_sentiment_inference_service_api_key: "Clé API pour l'API de sentiment"
- ai_sentiment_models: "Modèles à utiliser pour l'inférence. Le sentiment classe les publications dans la plage positif/neutre/négatif. Les émotions se classent dans la plage colère/dégoût/peur/joie/neutre/tristesse/surprise."
- ai_nsfw_detection_enabled: "Activez le module NSFW."
- ai_nsfw_inference_service_api_endpoint: "Adresse URL où l'API s'exécute pour le module NSFW"
- ai_nsfw_inference_service_api_key: "Clé API pour l'API NSFW"
- ai_nsfw_flag_automatically: "Signalez automatiquement les publications NSFW qui dépassent les seuils configurés."
- ai_nsfw_flag_threshold_general: "Seuil général pour qu'une image soit considérée comme NSFW."
- ai_nsfw_flag_threshold_drawings: "Seuil pour qu'un dessin soit considéré comme NSFW."
- ai_nsfw_flag_threshold_hentai: "Seuil pour qu'une image classée comme hentai soit considérée comme NSFW."
- ai_nsfw_flag_threshold_porn: "Seuil pour qu'une image classée comme pornographique soit considérée comme NSFW."
- ai_nsfw_flag_threshold_sexy: "Seuil pour qu'une image classée comme sexy soit considérée comme NSFW."
- ai_nsfw_models: "Modèles à utiliser pour l'inférence NSFW."
- ai_helper_enabled: "Activer l'assistant IA."
- composer_ai_helper_allowed_groups: "Les utilisateurs de ces groupes verront le bouton d'assistance IA dans le compositeur."
- ai_helper_allowed_in_pm: "Activez l'assistant IA du compositeur dans les messages privés."
- ai_helper_model: "Modèle à utiliser pour l'assistant IA."
- ai_helper_custom_prompts_allowed_groups: "Les utilisateurs de ces groupes verront l'option d'invite personnalisée dans l'assistant IA."
- ai_helper_automatic_chat_thread_title_delay: "Délai en minutes avant que l'assistant IA définisse automatiquement le titre du fil de discussion."
- ai_helper_automatic_chat_thread_title: "Définissez automatiquement les titres des fils de discussion en fonction du contenu du fil."
- ai_helper_illustrate_post_model: "Modèle à utiliser pour la fonction d'illustration de publication de l'assistant IA du compositeur"
- ai_helper_enabled_features: "Sélectionnez les fonctionnalités à activer dans l’assistant IA."
- post_ai_helper_allowed_groups: "Les groupes d'utilisateurs autorisés à accéder aux fonctionnalités d'AI Helper dans les publications"
- ai_helper_image_caption_model: "Sélectionnez le modèle à utiliser pour générer des légendes d'images"
- ai_auto_image_caption_allowed_groups: "Les utilisateurs de ces groupes peuvent activer ou désactiver les légendes automatiques des images."
- ai_embeddings_selected_model: "Utilisez le modèle sélectionné pour générer des intégrations."
- ai_embeddings_generate_for_pms: "Générez des intégrations pour les messages privés."
- ai_embeddings_semantic_related_topics_enabled: "Utilisez la recherche sémantique pour les sujets connexes."
- ai_embeddings_semantic_related_topics: "Nombre maximal de sujets à afficher dans la section des sujets connexes."
- ai_embeddings_backfill_batch_size: "Nombre d'intégrations à remplir toutes les 15 minutes."
- ai_embeddings_semantic_search_enabled: "Activez la recherche sémantique en pleine page."
- ai_embeddings_semantic_quick_search_enabled: "Activez l'option de recherche sémantique dans la fenêtre contextuelle du menu de recherche."
- ai_embeddings_semantic_related_include_closed_topics: "Inclure des sujets fermés dans les résultats de recherche sémantique"
- ai_embeddings_semantic_search_hyde_model: "Modèle utilisé pour développer des mots-clés afin d'obtenir de meilleurs résultats lors d'une recherche sémantique"
- ai_embeddings_per_post_enabled: Générer des intégrations pour chaque publication
- ai_summarization_model: "Modèle à utiliser pour les résumés"
- ai_custom_summarization_allowed_groups: "Les groupes autorisés à utiliser la création de nouveaux résumés."
- ai_pm_summarization_allowed_groups: "Groupes autorisés à créer et à afficher des résumés dans les MD."
- ai_summary_gists_enabled: "Générez automatiquement de brefs résumés des dernières réponses dans les sujets"
- ai_summary_gists_allowed_groups: "Les groupes sont autorisés à voir l'essentiel dans la liste des sujets d'actualité."
- ai_summary_backfill_maximum_topics_per_hour: "Nombre de résumés de sujets à compléter par heure."
- ai_bot_enabled: "Activez le module du robot IA."
- ai_bot_enable_chat_warning: "Afficher un avertissement lorsque le chat MP est lancé. Peut être remplacé en modifiant la chaîne de traduction : discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Lorsque le robot GPT aura accès aux messages privés, il répondra aux membres de ces groupes."
- ai_bot_debugging_allowed_groups: "Autoriser ces groupes à voir un bouton de débogage sur les publications qui affiche la demande et la réponse brutes de l'IA"
- ai_bot_public_sharing_allowed_groups: "Autorisez ces groupes à partager des messages personnels de l'IA avec le public via un lien unique accessible au public. Remarque : si votre site nécessite une connexion, les partages nécessiteront également une connexion."
- ai_bot_add_to_header: "Afficher un bouton dans l'en-tête pour démarrer une conversation avec un robot IA"
- ai_bot_github_access_token: "Jeton d'accès GitHub à utiliser avec les outils GitHub AI (requis pour la prise en charge de la recherche)"
- ai_stability_api_key: "Clé API pour l'API stable.ai"
- ai_stability_engine: "Moteur de génération d'images à utiliser pour l'API stability.ai"
- ai_stability_api_url: "Adresse URL de l'API stability.ai"
- ai_google_custom_search_api_key: "Clé API pour l'API Google Custom Search, voir : https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX pour l'API de recherche personnalisée Google"
- reviewables:
- reasons:
- flagged_by_toxicity: L'extension IA l'a signalé après l'avoir classé comme toxique.
- flagged_by_nsfw: L'extension IA l'a signalé après avoir classé au moins une des images jointes comme NSFW.
- reports:
- overall_sentiment:
- title: "Sentiment général"
- description: 'Le graphique compare les nombres de publications classées comme positives ou négatives. Ces nombres sont calculés lorsque les scores positifs ou négatifs dépassent le score seuil défini. Cela signifie que les publications neutres ne sont pas affichées. Les messages directs (MD) sont également exclus. Classé dans « cardiffnlp/twitter-roberta-base-sentiment-latest »'
- xaxis: "Positif (%)"
- yaxis: "Date"
- emotion_admiration:
- title: "\U0001F929 Admiration"
- description: "Publications classées selon l'émotion d'admiration via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_amusement:
- title: "\U0001F604 Amusement"
- description: "Publications classées selon l'émotion d'amusement via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_anger:
- title: "\U0001F620 Colère"
- description: "Publications classées selon l'émotion de colère via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_annoyance:
- title: "\U0001F612 Agacement"
- description: "Publications classées selon l'émotion d'agacement via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_approval:
- title: "\U0001F44D Approbation"
- description: "Publications classées selon l'émotion d'approbation via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_caring:
- title: "\U0001F917 Bienveillance"
- description: "Publications classées selon l'émotion de bienveillance via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_confusion:
- title: "\U0001F615 Confusion"
- description: "Publications classées selon l'émotion de confusion via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_curiosity:
- title: "\U0001F914 Curiosité"
- description: "Publications classées selon l'émotion de curiosité via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_desire:
- title: "\U0001F60D Désir"
- description: "Publications classées selon l'émotion de désir via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_disappointment:
- title: "\U0001F61E Déception"
- description: "Publications classées selon l'émotion de déception via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_disapproval:
- title: "\U0001F44E Désapprobation"
- description: "Publications classées selon l'émotion de désapprobation via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_disgust:
- title: "\U0001F922 Dégoût"
- description: "Publications classées selon l'émotion de dégoût via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_embarrassment:
- title: "\U0001F633 Embarras"
- description: "Publications classées selon l'émotion d'embarras via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_excitement:
- title: "\U0001F92A Excitation"
- description: "Publications classées selon l'émotion d'excitation via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_fear:
- title: "\U0001F628 Peur"
- description: "Publications classées selon l'émotion de peur via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_gratitude:
- title: "\U0001F64F Gratitude"
- description: "Publications classées selon l'émotion de gratitude via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_grief:
- title: "\U0001F622 Chagrin"
- description: "Publications classées selon l'émotion de chagrin via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_joy:
- title: "\U0001F60A Joie"
- description: "Publications classées selon l'émotion de joie via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_love:
- title: '❤️ Amour'
- description: "Publications classées selon l'émotion d'amour via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_nervousness:
- title: "\U0001F630 Nervosité"
- description: "Publications classées selon l'émotion de nervosité via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_neutral:
- title: "\U0001F610 Neutre"
- description: "Publications classées selon l'émotion de neutralité via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_optimism:
- title: "\U0001F31F Optimisme"
- description: "Publications classées selon l'émotion d'optimisme via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_pride:
- title: "\U0001F981 Fierté"
- description: "Publications classées selon l'émotion de fierté via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_realization:
- title: "\U0001F4A1 Réalisation"
- description: "Publications classées selon l'émotion de réalisation via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_relief:
- title: "\U0001F60C Soulagement"
- description: "Publications classées selon l'émotion de soulagement via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_remorse:
- title: "\U0001F614 Remords"
- description: "Publications classées selon l'émotion de remords via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_sadness:
- title: "\U0001F62D Tristesse"
- description: "Publications classées selon l'émotion de tristesse via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- emotion_surprise:
- title: "\U0001F632 Surprise"
- description: "Publications classées selon l'émotion de surprise via l'IA, en utilisant le modèle « Samlowe/Roberta-Base-Go_Emotions »."
- discourse_ai:
- ai_artifact:
- view_source: "Voir la source"
- view_changes: "Afficher les modifications"
- unknown_model: "Modèle d'IA inconnu"
- tools:
- custom_name: "%{name} (personnalisé)"
- presets:
- browse_web_jina:
- name: "Parcourir le Web (jina.ai)"
- exchange_rate:
- name: "Taux de change"
- stock_quote:
- name: "Cotation boursière (AlphaVantage)"
- image_generation:
- name: "Générateur d'images de flux (Together.ai)"
- empty_tool:
- name: "Repartir de zéro..."
- ai_helper:
- errors:
- completion_request_failed: "Une erreur s'est produite lors de la tentative de fournir des suggestions. Veuillez réessayer."
- prompts:
- translate: Traduire en %{language}
- generate_titles: Suggérer des titres de sujets
- proofread: Relisez le texte
- markdown_table: Générer un tableau Markdown
- custom_prompt: "Invite personnalisée"
- explain: "Expliquer"
- illustrate_post: "Illustrer la publication"
- replace_dates: "Dates intelligentes"
- painter:
- attribution:
- stable_diffusion_xl: "Image de Stable Diffusion XL"
- dall_e_3: "Image de DALL-E 3"
- image_caption:
- attribution: "Légende par l'IA"
- share_ai:
- read_more: "Lire la transcription complète"
- onebox_title: "Conversation IA avec %{llm_name}"
- formatted_excerpt: "Conversation IA avec %{llm_name} :\n %{excerpt}"
- title: "%{title} - Conversation IA - %{site_name}"
- errors:
- not_allowed: "Vous n'avez pas l'autorisation de partager ce sujet"
- other_people_in_pm: "Les messages personnels avec d'autres humains ne peuvent pas être partagés publiquement"
- other_content_in_pm: "Les messages personnels contenant des messages d'autres personnes ne peuvent pas être partagés publiquement"
- failed_to_share: "Échec du partage de la conversation"
- conversation_deleted: "Partage de conversation supprimé avec succès"
- spam_detection:
- flag_reason: "Signalé comme spam par Discourse AI"
- silence_reason: "Utilisateur mis en sourdine automatiquement par Discourse AI"
- invalid_error_type: "Le type d'erreur fourni n'est pas valide"
- unexpected: "Une erreur inattendue s'est produite"
- bot_user_update_failed: "Échec de la mise à jour de l'utilisateur du robot d'analyse anti-spam"
- ai_bot:
- reply_error: "Il semble que notre système ait rencontré un problème inattendu en essayant de répondre.\n\n[details='Détails de l'erreur']\n%{details}\n[/details]"
- default_pm_prefix: "[MD de robot IA sans titre]"
- personas:
- default_llm_required: "Le modèle LLM par défaut est requis avant d'activer Chat"
- cannot_delete_system_persona: "Les personnages système ne peuvent pas être supprimés, veuillez plutôt les désactiver"
- cannot_edit_system_persona: "Les personnages du système peuvent uniquement être renommés, vous ne pouvez pas modifier les outils ou l'invite du système, mais seulement les désactiver et en faire une copie"
- github_helper:
- name: "Assistant GitHub"
- description: "Robot IA spécialisé dans l'assistance aux tâches et questions liées à GitHub"
- general:
- name: Assistant du forum
- description: "Bot IA à usage général capable d'effectuer diverses tâches"
- artist:
- name: Artiste
- description: "Bot IA spécialisé dans la génération d'images"
- sql_helper:
- name: Assistant SQL
- description: "Bot IA spécialisé dans l'aide à la création de requêtes SQL sur cette instance Discourse"
- settings_explorer:
- name: Explorateur de paramètres
- description: "Bot IA spécialisé dans l'aide à l'exploration des paramètres du site Discourse"
- creative:
- name: Créatif
- description: "Bot IA sans intégration externe spécialisé dans les tâches créatives"
- dall_e3:
- name: "DALL-E 3"
- description: "Bot IA spécialisé dans la génération d'images utilisant DALL-E 3"
- discourse_helper:
- name: "Assistant Discourse"
- description: "Robot IA spécialisé dans l'aide aux tâches liées à Discourse"
- web_artifact_creator:
- name: "Créateur d'artefacts Web"
- description: "Robot IA spécialisé dans la création d'artefacts Web interactifs"
- custom_prompt:
- name: "Invite personnalisée"
- smart_dates:
- name: "Dates intelligentes"
- topic_not_found: "Résumé indisponible, sujet introuvable !"
- summarizing: "Synthèse du sujet"
- searching: "Recherche de : '%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "Nombre maximal de résultats"
- google:
- base_query:
- name: "Requête de recherche de base"
- description: "Requête de base à utiliser lors de la recherche. Exemples : « site:exemple.com » n'inclura que les résultats d'exemple.com, « before:2022-01-01 » n'inclura que les résultats de 2021 et avant. Ce texte est ajouté au début de la requête de recherche."
- read:
- read_private:
- name: "Lire en privé"
- description: "Autoriser l'accès à tous les sujets auxquels l'utilisateur a accès (par défaut, seuls les sujets publics sont inclus)"
- search:
- search_private:
- name: "Recherche privée"
- description: "Inclure tous les sujets auxquels l'utilisateur a accès dans les résultats de recherche (par défaut, seuls les sujets publics sont inclus)"
- max_results:
- name: "Nombre maximal de résultats"
- description: "Nombre maximal de résultats à inclure dans la recherche : si les règles par défaut sont vides, elles seront utilisées et le nombre sera mis à l'échelle en fonction du modèle utilisé. La valeur la plus élevée est 100."
- base_query:
- name: "Requête de recherche de base"
- description: "Requête de base à utiliser lors de la recherche. Exemple : « #urgent » ajoutera « #urgent » à la requête de recherche et inclura uniquement les sujets avec la catégorie ou l'étiquette correspondante."
- tool_summary:
- update_artifact: "Mettre à jour un artefact Web"
- create_artifact: "Créer un artefact Web"
- web_browser: "Parcourir le Web"
- github_search_files: "Fichiers de recherche GitHub"
- github_search_code: "Recherche de code GitHub"
- github_file_content: "Contenu du fichier GitHub"
- github_pull_request_diff: "Diff des pull requests GitHub"
- random_picker: "Sélecteur aléatoire"
- categories: "Répertorier les catégories"
- search: "Rechercher"
- tags: "Répertorier les étiquettes"
- time: "Heure"
- summarize: "Résumer"
- image: "Générer une image"
- google: "Rechercher sur Google"
- read: "Lire le sujet"
- setting_context: "Rechercher le contexte de configuration du site"
- schema: "Rechercher le schéma de la base de données"
- search_settings: "Recherche des paramètres du site"
- dall_e: "Générer une image"
- search_meta_discourse: "Méta-recherche Discourse"
- javascript_evaluator: "Évaluer JavaScript"
- tool_help:
- update_artifact: "Mettre à jour un artefact Web à l'aide du robot IA"
- create_artifact: "Créer un artefact Web à l'aide du robot IA"
- web_browser: "Parcourir la page Web à l'aide du robot IA"
- github_search_code: "Rechercher du code dans un dépôt GitHub"
- github_search_files: "Rechercher des fichiers dans un dépôt GitHub"
- github_file_content: "Récupérer le contenu des fichiers depuis un dépôt GitHub"
- github_pull_request_diff: "Récupérer un diff de pull request GitHub"
- random_picker: "Choisissez un nombre aléatoire ou un élément aléatoire d'une liste"
- categories: "Répertorier toutes les catégories visibles publiquement sur le forum"
- search: "Rechercher dans tous les sujets publics sur le forum"
- tags: "Répertorier toutes les étiquettes du forum"
- time: "Trouver l'heure dans différents fuseaux horaires"
- summary: "Résumer un sujet"
- image: "Générer une image à l'aide de Stable Diffusion"
- google: "Rechercher une requête sur Google"
- read: "Lire le sujet public sur le forum"
- setting_context: "Rechercher le contexte de configuration du site"
- schema: "Rechercher le schéma de la base de données"
- search_settings: "Paramètres du site de recherche"
- dall_e: "Générer une image à l'aide de DALL-E 3"
- search_meta_discourse: "Méta-recherche Discourse"
- javascript_evaluator: "Évaluer JavaScript"
- tool_description:
- update_artifact: "Mise à jour d'un artefact Web à l'aide du robot IA"
- web_browser: "Lecture : %{url}"
- github_search_files: "Recherche de « %{keywords} » dans %{repo}/%{branch}"
- github_search_code: "Recherche de « %{query} » dans %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "Contenu récupéré de %{file_paths} de %{repo_name}@%{branch}"
- random_picker: "Choix parmi %{options}, sélection : %{result}"
- read: "Lecture : %{title}"
- time: "L'heure (%{timezone}) est %{time}"
- summarize: "Résumé de %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "%{count} catégorie trouvée"
- other: "%{count} catégories trouvées"
- tags:
- one: "%{count} étiquette trouvée"
- other: "%{count} étiquettes trouvées"
- search:
- one: "%{count} résultat trouvé pour « %{query} »"
- other: "%{count} résultats trouvés pour « %{query} »"
- search_meta_discourse:
- one: "%{count} résultat trouvé pour « %{query} »"
- other: "%{count} résultats trouvés pour « %{query} »"
- google:
- one: "%{count} résultat trouvé pour « %{query} »"
- other: "%{count} résultats trouvés pour « %{query} »"
- setting_context: "Contexte de lecture pour : %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "%{count} résultat trouvé pour « %{query} »"
- other: "%{count} résultats trouvés pour « %{query} »"
- summarization:
- configuration_hint:
- one: "Configurez d'abord le paramètre « %{setting} »."
- other: "Configurez d'abord ces paramètres : %{settings}"
- chat:
- no_targets: "Il n'y a eu aucun message pendant la période sélectionnée."
- sentiment:
- reports:
- overall_sentiment: "Sentiment général (positif - négatif)"
- post_emotion:
- sadness: "Tristesse \U0001F622"
- surprise: "Surprise \U0001F631"
- neutral: "Neutre \U0001F610"
- fear: "Peur \U0001F628"
- anger: "Colère \U0001F621"
- joy: "Joie \U0001F600"
- disgust: "Dégoût \U0001F922"
- sentiment_analysis:
- positive: "Positif"
- negative: "Négatif"
- neutral: "Neutre"
- llm:
- configuration:
- disable_module_first: "Vous devez d'abord désactiver %{setting}."
- set_llm_first: "Réglez %{setting} en premier"
- model_unreachable: "Nous n'avons pas pu obtenir de réponse de ce modèle. Vérifiez d'abord vos paramètres."
- invalid_seeded_model: "Vous ne pouvez pas utiliser ce modèle avec cette fonctionnalité"
- must_select_model: "Vous devez d'abord sélectionner un LLM"
- endpoints:
- not_configured: "%{display_name} (non configuré)"
- configuration_hint:
- one: "Assurez-vous que le paramètre « %{settings} » a été configuré."
- other: "Assurez-vous que ces paramètres ont été configurés : %{settings}"
- delete_failed:
- one: "Nous n'avons pas pu supprimer ce modèle, car %{settings} l'utilise. Mettez à jour le paramètre et réessayez."
- other: "Nous n'avons pas pu supprimer ce modèle, car %{settings} l'utilisent. Mettez à jour les paramètres et réessayez."
- cannot_edit_builtin: "Vous ne pouvez pas modifier un modèle intégré."
- embeddings:
- delete_failed: "Ce modèle est actuellement utilisé. Mettez d'abord à jour `ai embeddings selected model`."
- cannot_edit_builtin: "Vous ne pouvez pas modifier un modèle intégré."
- configuration:
- disable_embeddings: "Vous devez d'abord désactiver « l'intégration de l'IA activée »."
- choose_model: "Définissez d'abord 'ai embeddings selected model'."
- llm_models:
- missing_provider_param: "%{param} ne peut pas être vide"
- bedrock_invalid_url: "Veuillez remplir tous les champs pour utiliser ce modèle."
- ai_staff_action_logger:
- updated: "mis à jour"
- removed: "supprimé"
- errors:
- quota_exceeded: "Vous avez dépassé le quota pour ce modèle. Veuillez réessayer dans %{relative_time}."
- quota_required: "Vous devez spécifier un nombre maximal de jetons ou d'utilisations pour ce modèle"
- no_query_specified: Le paramètre de requête est obligatoire, veuillez le spécifier.
- no_user_for_persona: Le personnage spécifié n'a pas d'utilisateur associé.
- persona_not_found: Le personnage spécifié n'existe pas. Vérifiez les paramètres persona_name ou persona_id.
- no_user_specified: Le nom d'utilisateur ou le paramètre user_unique_id est obligatoire. Veuillez le préciser.
- user_not_found: L'utilisateur spécifié n'existe pas. Vérifiez le paramètre du nom d'utilisateur.
- persona_disabled: Le personnage spécifié est désactivé. Vérifiez les paramètres persona_name ou persona_id.
- no_default_llm: Le personnage doit avoir un default_llm défini.
- user_not_allowed: L'utilisateur n'est pas autorisé à participer au sujet.
- prompt_message_length: Le message %{idx} dépasse la limite de 1 000 caractères.
diff --git a/config/locales/server.gl.yml b/config/locales/server.gl.yml
deleted file mode 100644
index 0dfbbd28..00000000
--- a/config/locales/server.gl.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-gl:
- reports:
- overall_sentiment:
- yaxis: "Data"
- emotion_neutral:
- title: "\U0001F610 Neutro"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Buscar"
- time: "Hora"
- sentiment:
- reports:
- post_emotion:
- neutral: "Neutro \U0001F610"
- sentiment_analysis:
- neutral: "Neutro"
- ai_staff_action_logger:
- updated: "actualizado"
- removed: "retirado"
diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml
deleted file mode 100644
index 2253bf88..00000000
--- a/config/locales/server.he.yml
+++ /dev/null
@@ -1,527 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-he:
- discourse_automation:
- ai:
- flag_types:
- review: "הוספת פוסט לתור הביקורת"
- spam: "סימון כזבל והסתרת הפוסט"
- spam_silence: "סימון כזבל, הסתרת הפוסט והשתקת המשתמש"
- scriptables:
- llm_tool_triage:
- title: מיון ראשוני של פוסטים באמצעות כלי בינה מלאכותית
- llm_triage:
- title: מיון ראשוני של פוסטים באמצעות בינה מלאכותית
- description: "מיון ראשוני של פוסטים באמצעות דגם שפה גדול"
- flagged_post: |
-
תגובה מהמודל:
-
%%LLM_RESPONSE%%
- הוזנקה על ידי הכלל %%AUTOMATION_NAME%%.
- llm_report:
- title: דוח תקופתי בעזרת בינה מלאכותית
- description: "דוח תקופתי שמבוסס על דגם שפה גדול"
- site_settings:
- discourse_ai_enabled: "הפעלת תוסף הבינה המלאכותית של Discourse."
- ai_artifact_security: "מערכת התוצרים של הבינה המלאכותית מייצרת חלוניות (IFRAME) עם קוד שניתן להריץ. מצב נוקשה מאלץ לחיצה נוספת כדי להריץ קוד. מצב Lax מריץ קוד ישירות. מצב משולב מאפשר למשתמש לספק data-ai-artifact-autorun שיופיע מיידית. מצב מושבת משבית את מערכת התוצרים."
- ai_toxicity_enabled: "הפעלת מודול הרעילות."
- ai_toxicity_inference_service_api_endpoint: "הכתובת בה נמצא ה־API הפעיל עבור מודול הרעילות"
- ai_toxicity_inference_service_api_key: "מפתח API ל־API של הרעילות"
- ai_toxicity_inference_service_api_model: "דגם לשימוש להסקת מסקנות. הדגם הרב־לשוני עובד עם איטלקית, צרפתית, רוסית, פורטוגלית, ספרדית וטורקית."
- ai_toxicity_flag_automatically: "לסמן פוסטים / הודעות צ׳אט שמעל הספים המוגדרים אוטומטית."
- ai_toxicity_flag_threshold_toxicity: "רעילות: הערה גסה, מזלזלת או בלתי הגיונית שעלולה לגרום לך לעזוב דיון או לוותר על שיתוף העמדה שלך"
- ai_toxicity_flag_threshold_severe_toxicity: "רעילות חמורה: הערה שטופת שנאה, תוקפנית או מזלזלת במיוחד שבהחלט עלולה לגרום לך לעזוב דיון או לוותר על שיתוף העמדה שלך"
- ai_toxicity_flag_threshold_obscene: "מגונה"
- ai_toxicity_flag_threshold_identity_attack: "מתקפת זהות"
- ai_toxicity_flag_threshold_insult: "עלבון"
- ai_toxicity_flag_threshold_threat: "איום"
- ai_toxicity_flag_threshold_sexual_explicit: "מיניות מפורשת"
- ai_toxicity_groups_bypass: "הפוסטים של משתמשים בקבוצות האלו לא יסווגו על ידי מודול הרעילות."
- ai_sentiment_enabled: "הפעלת מודול התחושה."
- ai_sentiment_inference_service_api_endpoint: "הכתובת בה נמצא ה־API הפעיל עבור מודול התחושה"
- ai_sentiment_inference_service_api_key: "מפתח API ל־API של התחושה"
- ai_sentiment_models: "מודלים לטובת הסקת מסקנות. תחושה מסווגת את הפוסט על המנעד של חיובי/נייטרלי/שלילי. רגש מסווג על המנעד של כעס/גועל/פחד/שמחה/נייטרליות/עצב/הפתעה."
- ai_nsfw_detection_enabled: "הפעלת מודול הפוגענות."
- ai_nsfw_inference_service_api_endpoint: "הכתובת בה נמצא ה־API הפעיל עבור מודול הפוגענות"
- ai_nsfw_inference_service_api_key: "מפתח API ל־API לתכנים פוגעניים"
- ai_nsfw_flag_automatically: "לסמן פוסטים פוגעניים שמעל הספים המוגדרים אוטומטית."
- ai_nsfw_flag_threshold_general: "סף כללי כדי שתמונה תיחשב לפוגענית."
- ai_nsfw_flag_threshold_drawings: "סף לציור כדי שייחשב לפוגעני."
- ai_nsfw_flag_threshold_hentai: "סף לתמונה שמוגדרת כהנטאי כדי שתיחשב פוגענית."
- ai_nsfw_flag_threshold_porn: "סף לתמונה שמוגדרת כארוטית כדי שתיחשב פוגענית."
- ai_nsfw_flag_threshold_sexy: "סף לתמונה שמוגדרת כמינית כדי שתיחשב פוגענית."
- ai_nsfw_models: "מודלים לשימוש להסקת פוגענות."
- ai_openai_api_key: "מפתח API ל־API של OpenAI. משמש רק ליצירת ועריכת תמונות. בשביל GPT יש להשתמש בלשונית הגדרת ה־LLM."
- ai_openai_image_generation_url: "כתובת ל־API ליצירת תמונות מבית OpenAI"
- ai_openai_image_edit_url: "כתובת ל־API לעריכת תמונות מבית OpenAI"
- ai_helper_enabled: "הפעלת מסייע הבינה המלאכותית."
- composer_ai_helper_allowed_groups: "משתמשים בקבוצות אלו יראו כפתור מסייע בינה מלאכותית במחבר ההודעות."
- ai_helper_allowed_in_pm: "הפעלת מסייע הבינה המלאכותית בכותב ההודעות בהודעות הפרטיות."
- ai_helper_model: "דגם לשימוש למסייע הבינה המלאכותית."
- ai_helper_custom_prompts_allowed_groups: "משתמשים בקבוצות אלה יראו את אפשרות הבקשה המותאמת אישית במסייע הבינה המלאכותית."
- ai_helper_automatic_chat_thread_title_delay: "השהייה בשניות לפני שמסייע הבינה המלאכותית מגדיר את כותרת שרשור הצ׳אט אוטומטית."
- ai_helper_automatic_chat_thread_title: "הגדרת כותרות שרשור הצ׳אט אוטומטית לפי תוכן השרשור."
- ai_helper_illustrate_post_model: "דגם לשימוש ליכולת הדמיית הפוסט של מסייע הבינה המלאכותית בכתיבה"
- ai_helper_enabled_features: "נא לבחור יכולות להפעלה במסייע הבינה המלאכותית."
- post_ai_helper_allowed_groups: "קבוצות משתמשים שמורשות לגשת ליכולות מסייע בינה מלאכותית בפוסטים"
- ai_helper_image_caption_model: "נא לבחור את הדגם שישמש ליצירת כותרות לתמונות"
- ai_auto_image_caption_allowed_groups: "משתמשים בקבוצות האלה יכולים להפעיל או לכבות כותרות אוטומטיות לתמונות."
- ai_embeddings_generate_for_pms: "יצירת הטמעות להודעות פרטיות."
- ai_embeddings_semantic_related_topics_enabled: "להשתמש בחיפוש סמנטי לנושאים קשורים."
- ai_embeddings_semantic_related_topics: "מספר הנושאים המרבי להצגה בסעיף הנושאים הקשורים."
- ai_embeddings_backfill_batch_size: "מספר ההטבעות למילוי חוזר כל 15 דקות."
- ai_embeddings_semantic_search_enabled: "הפעלת חיפוש סמנטי בעמוד מלא."
- ai_embeddings_semantic_quick_search_enabled: "הפעלת אפשרות חיפוש סמנטי בחלונית תפריט החיפוש הקופצת."
- ai_embeddings_semantic_related_include_closed_topics: "כולל נושאים סגורים בתוצאות החיפוש הסמנטיות"
- ai_embeddings_semantic_search_hyde_model: "הדגם שמשמש להרחבת מילות החיפוש לקבלת תוצאות טובות יותר במהלך חיפוש סמנטי"
- ai_embeddings_per_post_enabled: יצירת הטמעות בכל פוסט בנפרד
- ai_summarization_enabled: "הפעלת יכולת הסיכום"
- ai_summarization_model: "מודל לשימוש לסיכום"
- ai_summarization_persona: "דמות לשימוש ליכולת הסיכום"
- ai_custom_summarization_allowed_groups: "קבוצות שמורשות להשתמש ליצירת סיכומים חדשים."
- ai_pm_summarization_allowed_groups: "קבוצות שמורשות ליצור ולצפות בתקצירים בהודעות פרטיות."
- ai_summary_gists_enabled: "יצירת תקצירים של התגובות האחרונות בנושאים אוטומטית"
- ai_summary_gists_allowed_groups: "קבוצות שמורשות לראות gists ברשימת הנושאים החמים."
- ai_summary_backfill_maximum_topics_per_hour: "מספר תקצירי הנושאים למילוי חוזר בשעה."
- ai_bot_enabled: "הפעלת מודול בוט הבינה המלאכותית."
- ai_bot_enable_chat_warning: "הצגת אזהרה עם פתיחת שיח הודעות פרטיות. אפשר לדרוס את זה על ידי עריכת מחרוזת התרגום: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "כאשר לבוט של GPT יש גישה להודעות הפרטיות, הוא יגיב לחברים בקבוצות האלה."
- ai_bot_debugging_allowed_groups: "לאפשר לקבוצות האלו לצפות בכפתור ניפוי שיאות שמציג את בקשת ותגובה הבינה המלאכותית באופן גולמי"
- ai_bot_public_sharing_allowed_groups: "לאפשר לקבוצות האלה לשתף הודעות אישיות של בינה למאכותית עם הציבור באמצעות קישור ייחודי גלוי לציבור. הערה: אם האתר שלך דורש כניסה, גם שיתופים ידרשו כניסה."
- ai_bot_add_to_header: "הצגת כפתור בכותרת כדי לפתוח הודעה פרטית עם בוט בינה מלאכותית"
- ai_bot_github_access_token: "אסימון גישה ל־GitHub לשימוש עם כלי הבינה המלאכותית של GitHub (נחוץ לתמיכה בחיפוש)"
- ai_stability_api_key: "מפתח API ל־API של stability.ai"
- ai_stability_engine: "מנוע לחילול תמונות לשימוש עבור ה־API של stability.ai"
- ai_stability_api_url: "כתובת ל־API של stability.ai"
- ai_google_custom_search_api_key: "מפתח API ל־API החיפוש המותאם אישית של Google, ר׳: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX ל־API החיפוש המותאם אישית של Google"
- ai_discord_search_enabled: "הפעלת יכולת החיפוש ב־Discord"
- ai_discord_app_id: "המזהה של יישום ה־Discord שברצונך לחבר אליו את החיפוש ב־Discord"
- ai_discord_app_public_key: "המפתח הציבורי של יישום ה־Discord שברצונך לחבר אליו את החיפוש ב־Discord"
- ai_discord_search_mode: "נא לבחור את מצב החיפוש לחיפוש ב־Discord"
- ai_discord_search_persona: "הדמות לשימוש בחיפוש Discord."
- ai_discord_allowed_guilds: "גילדות (שרתי) Discord בהן מותר לבוט לחפש"
- reviewables:
- reasons:
- flagged_by_toxicity: תוסף הבינה המלאכותית סימן את זה לאחר סיווג כרעיל.
- flagged_by_nsfw: תוסף הבינה המלאכותית סימן את זה לאחר סיווג לפחות אחת התמונות כפוגעניות.
- reports:
- sentiment_analysis:
- title: "ניתוח הבעות"
- overall_sentiment:
- title: "רגש כללי"
- description: 'התרשים משווה את מספר הפוסטים שמסווגים כחיוביים או שליליים. אלו מחושבים כאשר ניקוד חיובי או שלילי הוא גדול מסף הניקוד המוגדר. משמעות הדבר היא שפוסטים נייטרליים לא מופיעים. הודעות אישיות מוחרגות גם כן. מסווגות באמצעות „cardiffnlp/twitter-roberta-base-sentiment-latest”'
- xaxis: "חיובי(%)"
- yaxis: "תאריך"
- emotion_admiration:
- title: "\U0001F929 הערצה"
- description: "פוסטים שסווגו עם הרגש הערצה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_amusement:
- title: "\U0001F604 שעשוע"
- description: "פוסטים שסווגו עם הרגש שעשוע באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_anger:
- title: "\U0001F620 כעס"
- description: "פוסטים שסווגו עם הרגש כעס באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_annoyance:
- title: "\U0001F612 מורת רוח"
- description: "פוסטים שסווגו עם הרגש מורת רוח באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_approval:
- title: "\U0001F44D הסכמה"
- description: "פוסטים שסווגו עם הרגש הסכמה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_caring:
- title: "\U0001F917 אכפתיות"
- description: "פוסטים שסווגו עם הרגש אכפתיות באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_confusion:
- title: "\U0001F615 בלבול"
- description: "פוסטים שסווגו עם הרגש בלבול באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_curiosity:
- title: "\U0001F914 סקרנות"
- description: "פוסטים שסווגו עם הרגש סקרנות באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_desire:
- title: "\U0001F60D תשוקה"
- description: "פוסטים שסווגו עם הרגש תשוקה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_disappointment:
- title: "\U0001F61E אכזבה"
- description: "פוסטים שסווגו עם הרגש אכזבה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_disapproval:
- title: "\U0001F44E אי־הסכמה"
- description: "פוסטים שסווגו עם הרגש אי־הסכמה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_disgust:
- title: "\U0001F922 גועל"
- description: "פוסטים שסווגו עם הרגש גועל באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_embarrassment:
- title: "\U0001F633 מבוכה"
- description: "פוסטים שסווגו עם הרגש מבוכה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_excitement:
- title: "\U0001F92A התרגשות"
- description: "פוסטים שסווגו עם הרגש התרגשות באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_fear:
- title: "\U0001F628 פחד"
- description: "פוסטים שסווגו עם הרגש פחד באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_gratitude:
- title: "\U0001F64F הכרת תודה"
- description: "פוסטים שסווגו עם הרגש הכרת תודה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_grief:
- title: "\U0001F622 צער"
- description: "פוסטים שסווגו עם הרגש צער באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_joy:
- title: "\U0001F60A הנאה"
- description: "פוסטים שסווגו עם הרגש הנאה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_love:
- title: '❤️ אהבה'
- description: "פוסטים שסווגו עם הרגש אהבה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_nervousness:
- title: "\U0001F630 עצבנות"
- description: "פוסטים שסווגו עם הרגש עצבנות באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_neutral:
- title: "\U0001F610 נייטרלי"
- description: "פוסטים שסווגו עם הרגש נייטרלי באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_optimism:
- title: "\U0001F31F אופטימיות"
- description: "פוסטים שסווגו עם הרגש אופטימיות באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_pride:
- title: "\U0001F981 גאווה"
- description: "פוסטים שסווגו עם הרגש גאווה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_realization:
- title: "\U0001F4A1 תובנה"
- description: "פוסטים שסווגו עם הרגש תובנה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_relief:
- title: "\U0001F60C הקלה"
- description: "פוסטים שסווגו עם הרגש הקלה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_remorse:
- title: "\U0001F614 חרטה"
- description: "פוסטים שסווגו עם הרגש חרטה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_sadness:
- title: "\U0001F62D עצב"
- description: "פוסטים שסווגו עם הרגש עצב באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- emotion_surprise:
- title: "\U0001F632 הפתעה"
- description: "פוסטים שסווגו עם הרגש הפתעה באמצעות בינה מלאכותית, בעזרת המודל ‚SamLowe/roberta-base-go_emotions’."
- discourse_ai:
- ai_artifact:
- view_source: "הצגת המקור"
- view_changes: "הצגת שינויים"
- change_description: "עריכת התיאור"
- unknown_model: "דגם בינה מלאכותית לא ידוע"
- tools:
- custom_name: "%{name} (מותאם אישית)"
- presets:
- browse_web_jina:
- name: "גלישה באינטרנט (jina.ai)"
- exchange_rate:
- name: "שער חליפין"
- stock_quote:
- name: "מחיר המניה (AlphaVantage)"
- image_generation:
- name: "מחולל תמונות Flux (Together.ai)"
- empty_tool:
- name: "התחלה מאפס…"
- name:
- characters: "חייב לכלול מספרים, אותיות וקווים תחתיים בלבד"
- ai_helper:
- errors:
- completion_request_failed: "משהו השתבש במהלך הניסיון לספק הצעות. נא לנסות שוב."
- prompts:
- translate: תרגום ל%{language}
- generate_titles: הצעת כותרות לנושא
- proofread: הגהת הטקסט
- markdown_table: יצירת טבלה ב־Markdown
- custom_prompt: "בקשה מותאמת אישית"
- explain: "הסבר"
- illustrate_post: "איור פוסט"
- replace_dates: "תאריכים חכמים"
- painter:
- attribution:
- stable_diffusion_xl: "תמונה מאת Stable Diffusion XL"
- dall_e_3: "תמונה מאת DALL-E 3"
- image_caption:
- attribution: "הכותרת נוספה ע״י בינה מלאכותית"
- share_ai:
- read_more: "הצגת התמליל המלא"
- onebox_title: "דיון בינה מלאכותית עם %{llm_name}"
- formatted_excerpt: "דיון בינה מלאכותית עם %{llm_name}:\n %{excerpt}"
- title: "%{title} - דיון בינה מלאכותית - %{site_name}"
- errors:
- not_allowed: "אין לך הרשאה לשתף את הנושא הזה"
- other_people_in_pm: "אי אפשר לשתף הודעות פרטיות עם אנשים אחרים באופן ציבורי"
- other_content_in_pm: "אי אפשר לשתף הודעות שמכילות פוסטים של אנשים אחרים באופן ציבורי"
- failed_to_share: "שיתוף השיחה נכשל"
- conversation_deleted: "שיתוף השיחה נמחק בהצלחה"
- spam_detection:
- flag_reason: "סומן כספאם ע״י בינה מלאכותית של Discourse"
- silence_reason: "המשתמש הושתק אוטומטית על ידי בינה מלאכותית של Discourse"
- invalid_error_type: "סוג השגיאה שסופק לא תקין"
- unexpected: "אירעה שגיאה בלתי צפויה"
- ai_bot:
- reply_error: "נראה שהמערכת שלך נתקלה בקשיים לא צפויים בעת הניסיון להשיב, עמך הסליחה.\n\n[details='פרטי השגיאה']\n%{details}\n[/details]"
- default_pm_prefix: "[הודעה פרטית של בינה מלאכותית ללא כותרת]"
- thinking: "בתהליך חשיבה…"
- personas:
- default_llm_required: "ברירת מחדל של מודל שפה הגדול נחוצה בטרם הפעלת הצ׳אט"
- cannot_delete_system_persona: "אי אפשר למחוק דמויות מערכת, נא להשבית אותן במקום"
- cannot_edit_system_persona: "לדמויות מערכת אפשר רק לשנות את השם, אסור לערוך כלים או את בקשת הבסיס של המערכת, במקום יש להשבית ולשבט"
- cannot_have_duplicate_tools: "לא יכולים להיות כלים כפולים"
- github_helper:
- name: "מסייע GitHub"
- description: "בוט בינה מלאכותית שמסייע במשימות ובשאלות שקשורות ב־GitHub"
- general:
- name: מסייע פורומים
- description: "בוט בינה מלאכותית למטרות כלליות שיכול לבצע מגוון משימות"
- artist:
- name: אמן
- description: "בוט בינה מלאכותית שמתמחה ביצירת תמונות"
- designer:
- name: מעצב
- description: "בוט בינה מלאכותית שמתמחה ביצירת ועריכת תמונות"
- sql_helper:
- name: מסייע SQL
- description: "בוט בינה מלאכותית שמתמחה בסיוע בכתיבת שאילתות SQL במופע ה־Discourse הזה"
- settings_explorer:
- name: סייר הגדרות
- description: "בוט בינה מלאכותית שמתמחה בסיור בהגדרות אתר ה־Discourse"
- creative:
- name: יצירה
- description: "בוט בינה מלאכותית ללא שילובים חיצוניים שמתמחה ביצירת משימות יצירתיות"
- dall_e3:
- name: "DALL-E 3"
- description: "בוט בינה מלאכותית שמתמחה ביצירת תמונות באמצעות DALL-E 3"
- discourse_helper:
- name: "מסייע Discourse"
- description: "בוט בינה מלאכותית שמתמחה בסיוע במשימות שקשורות ב־Discourse"
- web_artifact_creator:
- name: "יוצר תוצרים דפדפניים"
- description: "בוט בינה מלאכותית שמתמחה ביצירת תוצרים אינטרנטיים אינטראקטיביים"
- summarizer:
- name: "מסכם"
- short_summarizer:
- name: "מסכם (מקוצר)"
- custom_prompt:
- name: "בקשה מותאמת אישית"
- smart_dates:
- name: "תאריכים חכמים"
- topic_not_found: "תקציר לא זמין, לא נמצא נושא!"
- summarizing: "הנושא מסוכם"
- searching: "חיפוש אחר: ‚%{query}’"
- tool_options:
- researcher:
- researcher_llm:
- name: "LLM"
- max_results:
- name: "מספר תוצאות מרבי"
- create_artifact:
- creator_llm:
- name: "LLM"
- description: "מודל שפה לשימוש ליצירת תוצרים"
- update_artifact:
- editor_llm:
- name: "LLM"
- description: "מודל שפה לשימוש לעריכת תוצרים"
- update_algorithm:
- name: "עדכון אלגוריתם"
- description: "לבקש מ־LLM להחליף לחלוטין או להשתמש ב־diff (הבדלים) כדי לעדכן"
- do_not_echo_artifact:
- description: "יגביל את העלויות אבל יפגום ביעילות עדכוני התוצרים"
- google:
- base_query:
- name: "שאילתת חיפוש בסיסית"
- description: "שאילתת בסיס לשימוש בחיפוש. דוגמאות: ‚site:example.com’ יכלול תוצאות מ־example.com, before:2022-01-01 יכלול תוצאות מ־2021 ולפני כן. הטקסט הזה נוסף לשאילתת החיפוש."
- read:
- read_private:
- name: "קריאה פרטית"
- description: "לאפשר גישה לכל הנושאים שיש למשתמש גישה אליהם (כברירת מחדל רק נושאים ציבוריים כלולים)"
- search:
- search_private:
- name: "חיפוש פרטי"
- description: "לכלול את כל הנושאים שלמשתמש יש גישה אליהם בתוצאות החיפוש (כברירת המחדל רק נושאים ציבוריים כלולים)"
- max_results:
- name: "מספר תוצאות מרבי"
- description: "המספר המרבי של תוצאות שיכללו בחיפוש - אם ייעשה שימוש בכללי ברירת מחדל ריקים והספירה תשתנה בהתאם לדגם שבשימוש. הערך הגבוה ביותר הוא 100."
- base_query:
- name: "שאילתת חיפוש בסיסית"
- description: "שאילתת בסיס לשימוש בעת חיפוש. למשל: ‚#urgent’ יוסיף את ‚#urgent’ לשאילתת החיפוש ויכלול רק נושאים עם הקטגוריה או התגית urgent (דחוף)."
- tool_summary:
- read_artifact: "קריאת תוצר דפדפני"
- update_artifact: "עדכון תוצר דפדפני"
- create_artifact: "יצירת תוצר דפדפני"
- web_browser: "גלישה באינטרנט"
- github_search_files: "חיפוש קבצים ב־GitHub"
- github_search_code: "חיפוש קוד ב־GitHub"
- github_file_content: "תוכן קובץ ב־GitHub"
- github_pull_request_diff: "הבדל בבקשת דחיפה ב־GitHub"
- random_picker: "בורר אקראי"
- categories: "הצגת קטגוריות"
- search: "חיפוש"
- tags: "הצגת תגיות"
- time: "מועד"
- summarize: "סיכום"
- image: "יצירת תמונה"
- google: "חיפוש ב־Google"
- read: "קריאת נושא"
- setting_context: "חיפוש הקשר הגדרות אתר"
- schema: "חיפוש סכמת מסד נתונים"
- search_settings: "מתבצע חיפוש בהגדרות האתר"
- dall_e: "יצירת תמונה"
- search_meta_discourse: "חיפוש ב־Meta Discrouse"
- javascript_evaluator: "שערוך JavaScript"
- create_image: "יצירת תמונה"
- edit_image: "עריכת תמונה"
- researcher_dry_run: "המחקר בהכנה"
- tool_help:
- read_artifact: "לקרוא תוצר דפדפני באמצעות בוט הבינה המלאכותית"
- update_artifact: "עדכון תוצר דפדפני באמצעות בוט הבינה המלאכותית"
- create_artifact: "יצירת תוצר דפדפני באמצעות בוט הבינה המלאכותית"
- web_browser: "גלישה באינטרנט באמצעות בוט בינה מלאכותית"
- github_search_code: "חיפוש אחר קוד במאגר GitHub"
- github_search_files: "חיפוש אחר קבצים במאגר GitHub"
- github_file_content: "משיכת תוכן של קבצים ממאגר ב־GitHub"
- github_pull_request_diff: "משיכת הבדל לבקשת דחיפה ב־GitHub"
- random_picker: "נא לבחור מספר אקראי או רכיב אקראי מרשימה"
- categories: "הצגת כל הקטגוריות החשופות לציבור בפורום"
- search: "חיפוש בכל הנושאים הציבוריים בפורום"
- tags: "הצגת כל התגיות בפורום"
- time: "איתור זמן באזורי זמן שונים"
- summary: "סיכום נושא"
- image: "יצירת תמונה באמצעות Stable Diffusion"
- create_image: "יצירת תמונה עם מודל התמונות של GPT מבית OpenAI"
- edit_image: "עריכת תמונה עם מודל התמונות של GPT מבית OpenAI"
- google: "חיפוש שאילתה ב־Google"
- read: "הצגת נושא ציבורי בפורום"
- setting_context: "חיפוש הקשר הגדרות אתר"
- schema: "חיפוש סכמת מסד נתונים"
- search_settings: "חיפוש בהגדרות האתר"
- dall_e: "יצירת תמונה באמצעות DALL-E 3"
- search_meta_discourse: "חיפוש ב־Meta Discrouse"
- javascript_evaluator: "שערוך JavaScript"
- tool_description:
- read_artifact: "לקרוא תוצר דפדפני באמצעות בוט הבינה המלאכותית"
- update_artifact: "עודכן תוצר דפדפני באמצעות בוט הבינה המלאכותית"
- create_artifact: "נוצר תוצר דפדפני: %{name} - %{specification}"
- web_browser: "קורא את %{url}"
- github_search_files: "בוצע חיפוש אחר ‚%{keywords}’ בתוך %{repo}/%{branch}"
- github_search_code: "בוצע חיפוש אחר ‚%{query}’ בתוך %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "התקבל תוכן של %{file_paths} מתוך %{repo_name}@%{branch}"
- random_picker: "בחירה מתוך %{options}, נבחר: %{result}"
- read: "קריאה: %{title}"
- time: "השעה ב־%{timezone} היא %{time}"
- summarize: "%{title} סוכם"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "נמצאה קטגוריה"
- two: "נמצאו שתי קטגוריות"
- many: "נמצאו %{count} קטגוריות"
- other: "נמצאו %{count} קטגוריות"
- tags:
- one: "נמצאה תגית"
- two: "נמצאו שתי תגיות"
- many: "נמצאו %{count} תגיות"
- other: "נמצאו %{count} תגיות"
- search:
- one: "נמצאה תוצאה לחיפוש אחר ‚%{query}’"
- two: "נמצאו שתי תוצאות לחיפוש אחר ‚%{query}’"
- many: "נמצאו %{count} תוצאות לחיפוש אחר ‚%{query}’"
- other: "נמצאו %{count} תוצאות לחיפוש אחר ‚%{query}’"
- search_meta_discourse:
- one: "נמצאה תוצאה לחיפוש אחר ‚%{query}’"
- two: "נמצאו שתי תוצאות לחיפוש אחר ‚%{query}’"
- many: "נמצאו %{count} תוצאות לחיפוש אחר ‚%{query}’"
- other: "נמצאו %{count} תוצאות לחיפוש אחר ‚%{query}’"
- google:
- one: "נמצאה תוצאה לחיפוש אחר ‚%{query}’"
- two: "נמצאו שתי תוצאות לחיפוש אחר ‚%{query}’"
- many: "נמצאו %{count} תוצאות לחיפוש אחר ‚%{query}’"
- other: "נמצאו %{count} תוצאות לחיפוש אחר ‚%{query}’"
- setting_context: "נקרא הקשר עבור: %{setting_name}"
- schema: "%{tables}"
- researcher_dry_run:
- one: "יעדים מוצעים: %{goals}\n\nנמצא פוסט שעונה על ‚%{filter}’"
- two: "יעדים מוצעים: %{goals}\n\nנמצאו שני פוסטים שעונים על ‚%{filter}’"
- many: "יעדים מוצעים: %{goals}\n\nנמצאו %{count} פוסטים שעונים על ‚%{filter}’"
- other: "יעדים מוצעים: %{goals}\n\nנמצאו %{count} פוסטים שעונים על ‚%{filter}’"
- researcher:
- one: "במהלך מחקר: %{goals}\n\nנמצא פוסט שעונה על ‚%{filter}’"
- two: "במהלך מחקר: %{goals}\n\nנמצאו שני פוסטים שעונים על ‚%{filter}’"
- many: "במהלך מחקר: %{goals}\n\nנמצאו %{count} פוסטים שעונים על ‚%{filter}’"
- other: "במהלך מחקר: %{goals}\n\nנמצאו %{count} פוסטים שעונים על ‚%{filter}’"
- search_settings:
- one: "נמצאה תוצאה לחיפוש אחר ‚%{query}’"
- two: "נמצאו שתי תוצאות לחיפוש אחר ‚%{query}’"
- many: "נמצאו %{count} תוצאות לחיפוש אחר ‚%{query}’"
- other: "נמצאו %{count} תוצאות לחיפוש אחר ‚%{query}’"
- summarization:
- configuration_hint:
- one: "יש להגדיר את ההגדרה `%{setting}` תחילה."
- two: "יש להגדיר את ההגדרות האלו תחילה: %{settings}."
- many: "יש להגדיר את ההגדרות האלו תחילה: %{settings}."
- other: "יש להגדיר את ההגדרות האלו תחילה: %{settings}."
- chat:
- no_targets: "לא היו הודעות במהלך התקופה הנבחרת."
- sentiment:
- reports:
- overall_sentiment: "חוויה כוללת (חיובית - שלילית)"
- post_emotion:
- sadness: "עצב \U0001F622"
- surprise: "הפתעה \U0001F631"
- neutral: "נייטרלי \U0001F610"
- fear: "פחד \U0001F628"
- anger: "כעס \U0001F621"
- joy: "הנאה \U0001F600"
- disgust: "גועל \U0001F922"
- sentiment_analysis:
- positive: "חיובי"
- negative: "שלילי"
- neutral: "נייטרלי"
- llm:
- configuration:
- disable_module_first: "קודם צריך להשבית את %{setting}."
- set_llm_first: "קודם יש להגדיר את %{setting}"
- model_unreachable: "לא הצלחנו לקבל תגובה מהמודל הזה. נא לוודא שההגדרות שלך נכונות קודם."
- invalid_seeded_model: "אי אפשר להשתמש במודל הזה עם היכולת הזאת"
- must_select_model: "קודם יש לבחור LLM"
- endpoints:
- not_configured: "%{display_name} (לא מוגדר)"
- configuration_hint:
- one: "נא לוודא שההגדרה `%{settings}` הוגדרה."
- two: "נא לוודא שההגדרות האלו הוגדרו: %{settings}"
- many: "נא לוודא שההגדרות האלו הוגדרו: %{settings}"
- other: "נא לוודא שההגדרות האלו הוגדרו: %{settings}"
- delete_failed:
- one: "לא הצלחנו למחוק את המודל הזה כי %{settings} משתמש בו. נא לעדכן את ההגדרות לנסות שוב."
- two: "לא הצלחנו למחוק את המודל הזה כי %{settings} משתמשים בו. נא לעדכן את ההגדרות לנסות שוב."
- many: "לא הצלחנו למחוק את המודל הזה כי %{settings} משתמשים בו. נא לעדכן את ההגדרות לנסות שוב."
- other: "לא הצלחנו למחוק את המודל הזה כי %{settings} משתמשים בו. נא לעדכן את ההגדרות לנסות שוב."
- cannot_edit_builtin: "אי אפשר לערוך מודל מובנה."
- embeddings:
- delete_failed: "זה המודל שנמצא בשימוש. יש לעדכן את `ai embeddings selected model`."
- cannot_edit_builtin: "אי אפשר לערוך מודל מובנה."
- configuration:
- disable_embeddings: "יש להשבית קודם את ‚הטמעת בינה מלאכותית פעילה’."
- invalid_config: "בחרת אפשרות שגויה."
- choose_model: "יש לעדכן את ‚ai embeddings selected model’."
- llm_models:
- missing_provider_param: "%{param} לא יכול להישאר ריק"
- bedrock_invalid_url: "נא למלא את כל השדות די להשתמש במודל הזה."
- ai_staff_action_logger:
- updated: "עודכן"
- removed: "הוסרה"
- errors:
- quota_exceeded: "חרגת מהמכסה למודל הזה. נא לנסות שוב בעוד %{relative_time}."
- quota_required: "יש לציין את כמות האסימונים המרבית לשימוש למודל הזה"
- no_query_specified: משתנה השאילתה נחוץ, נא לציין אותו.
- no_user_for_persona: לדמות שצוינה אין משתמש שמשויך אליה.
- persona_not_found: הדמות שצוינה לא קיימת. נא לבדוק את המשתנים persona_name או persona_id.
- no_user_specified: המשתנים username או user_unique_id נחוצים, נא לציין לפחות אחד מהם.
- user_not_found: המשתמש שצוין לא קיים. נא לבדוק את המשתנה username.
- persona_disabled: הדמות שצוינה הושבתה. נא לבדוק את המשתנים persona_name או persona_id.
- no_default_llm: לדמות חייב להיות מוגדר default_llm (מודל ברירת מחדל).
- user_not_allowed: המשתמש לא מורשה להשתתף בנושא.
- prompt_message_length: ההודעה %{idx} חורגת ממגבלת 1000 התווים.
- dashboard:
- problem:
- ai_llm_status: "מודל השפה הגדול: %{model_name} נתקל בקשיים. נא לגשת לעמוד ההגדרות של המודל."
diff --git a/config/locales/server.hr.yml b/config/locales/server.hr.yml
deleted file mode 100644
index a6f10423..00000000
--- a/config/locales/server.hr.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-hr:
- reports:
- overall_sentiment:
- yaxis: "Datum"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Pretraživanje"
- time: "Vrijeme"
- summarize: "Rezimirati"
- ai_staff_action_logger:
- updated: "ažurirano"
- removed: "uklonjeno"
diff --git a/config/locales/server.hu.yml b/config/locales/server.hu.yml
deleted file mode 100644
index a91618d1..00000000
--- a/config/locales/server.hu.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-hu:
- reports:
- overall_sentiment:
- yaxis: "Dátum"
- emotion_neutral:
- title: "\U0001F610 Semleges"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Keresés"
- tags: "Címkék listázása"
- time: "Idő"
- summarize: "Összefoglalás"
- sentiment:
- reports:
- post_emotion:
- neutral: "Semleges \U0001F610"
- sentiment_analysis:
- neutral: "Semleges"
- ai_staff_action_logger:
- updated: "frissítve"
- removed: "eltávolítva"
diff --git a/config/locales/server.hy.yml b/config/locales/server.hy.yml
deleted file mode 100644
index 779f3871..00000000
--- a/config/locales/server.hy.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-hy:
- reports:
- overall_sentiment:
- yaxis: "Ամսաթիվ"
- emotion_neutral:
- title: "\U0001F610 Նեյտրալ"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Որոնում"
- time: "Ժամ"
- sentiment:
- reports:
- post_emotion:
- neutral: "Նեյտրալ \U0001F610"
- sentiment_analysis:
- neutral: "Նեյտրալ"
- ai_staff_action_logger:
- updated: "թարմացված"
- removed: "հեռացվել է"
diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml
deleted file mode 100644
index 4efe05f1..00000000
--- a/config/locales/server.id.yml
+++ /dev/null
@@ -1,75 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-id:
- discourse_automation:
- scriptables:
- llm_triage:
- title: Triase posting menggunakan AI
- description: "Triase posting menggunakan model bahasa besar"
- llm_report:
- title: Laporan berkala menggunakan AI
- description: "Laporan berkala berdasarkan model bahasa besar"
- site_settings:
- discourse_ai_enabled: "Aktifkan plugin AI discourse."
- ai_toxicity_enabled: "Aktifkan modul toksisitas."
- ai_toxicity_inference_service_api_endpoint: "URL tempat API berjalan untuk modul toxicity"
- ai_toxicity_inference_service_api_key: "Kunci API untuk API toxicity"
- ai_toxicity_inference_service_api_model: "Model yang digunakan untuk inferensi. Model multibahasa dapat digunakan dengan bahasa Italia, Prancis, Rusia, Portugis, Spanyol, dan Turki."
- ai_toxicity_flag_automatically: "Secara otomatis menandai posting/pesan obrolan yang berada di atas ambang batas yang dikonfigurasi."
- ai_toxicity_flag_threshold_toxicity: "Toxicity: komentar kasar, tidak sopan, atau tidak masuk akal yang kemungkinan besar akan membuat Anda meninggalkan diskusi atau menyerah dalam menyampaikan sudut pandang Anda"
- ai_toxicity_flag_threshold_severe_toxicity: "Toxicity Parah: komentar yang sangat penuh kebencian, agresif, atau tidak sopan yang kemungkinan besar akan membuat Anda meninggalkan diskusi atau menyerah dalam membagikan sudut pandang Anda"
- ai_toxicity_flag_threshold_obscene: "Tidak senonoh"
- ai_toxicity_flag_threshold_identity_attack: "Serangan Identitas"
- ai_toxicity_flag_threshold_insult: "Penghinaan"
- ai_toxicity_flag_threshold_threat: "Ancaman"
- ai_toxicity_flag_threshold_sexual_explicit: "Eksplisit Seksual"
- ai_toxicity_groups_bypass: "Posting pengguna di grup tersebut tidak akan diklasifikasikan berdasarkan modul toxicity."
- ai_sentiment_enabled: "Aktifkan modul sentiment."
- ai_sentiment_inference_service_api_endpoint: "URL tempat API berjalan untuk modul sentiment"
- ai_sentiment_inference_service_api_key: "Kunci API untuk API sentiment"
- ai_sentiment_models: "Model yang digunakan untuk inferensi. Sentiment menglasifikasikan posting pada ruang positif/netral/negatif. Emosi dikelompokkan ke dalam ruang marah/jijik/takut/gembira/netral/sedih/kejutan."
- ai_nsfw_detection_enabled: "Aktifkan modul NSFW."
- ai_nsfw_inference_service_api_endpoint: "URL tempat API beroperasi untuk modul NSFW"
- ai_nsfw_inference_service_api_key: "Kunci API untuk API NSFW"
- ai_nsfw_flag_automatically: "Secara otomatis menandai postingan NSFW yang berada di atas ambang batas yang dikonfigurasi."
- ai_nsfw_flag_threshold_general: "Ambang Batas Umum agar suatu gambar dianggap NSFW."
- ai_nsfw_models: "Model yang digunakan untuk inferensi NSFW."
- ai_bot_public_sharing_allowed_groups: "Izinkan grup-grup ini untuk membagikan pesan pribadi AI kepada publik melalui tautan unik yang tersedia untuk umum. Catatan: jika situs Anda memerlukan login, berbagi juga akan memerlukan login."
- reports:
- overall_sentiment:
- yaxis: "Tanggal"
- discourse_ai:
- ai_artifact:
- view_changes: "Lihat Perubahan"
- ai_helper:
- prompts:
- custom_prompt: "Perintah Khusus"
- image_caption:
- attribution: "Keterangan oleh AI"
- ai_bot:
- default_pm_prefix: "[PM bot AI tanpa judul]"
- personas:
- default_llm_required: "Model LLM default diperlukan sebelum mengaktifkan Obrolan"
- tool_options:
- search:
- search_private:
- name: "Pencarian Pribadi"
- description: "Sertakan semua topik yang dapat diakses pengguna dalam hasil pencarian (secara bawaan hanya topik publik yang disertakan)"
- tool_summary:
- github_search_files: "File pencarian GitHub"
- search: "Cari"
- time: "Waktu"
- summarize: "Meringkas"
- tool_help:
- github_search_files: "Cari file di repositori GitHub"
- summary: "Meringkas suatu topik"
- tool_description:
- github_search_files: "Mencari '%{keywords}' di %{repo}/%{branch}"
- github_search_code: "Mencari '%{query}' di %{repo}"
- summarization:
- chat:
- no_targets: "Tidak ada pesan selama periode yang dipilih."
diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml
deleted file mode 100644
index 33527bfa..00000000
--- a/config/locales/server.it.yml
+++ /dev/null
@@ -1,445 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-it:
- discourse_automation:
- ai:
- flag_types:
- review: "Aggiungi il messaggio alla coda di revisione"
- spam: "Segnala come spam e nascondi il post"
- spam_silence: "Segnala come spam, nascondi il post e silenzia l'utente"
- scriptables:
- llm_triage:
- title: Triage dei messaggi con IA
- description: "Triage dei messaggi con LLM"
- flagged_post: |
-
Risposta dal modello:
-
%%LLM_RESPONSE%%
- Innescato dalla regola %%AUTOMATION_NAME%%.
- llm_report:
- title: Report periodico con IA
- description: "Report periodico basato su LLM"
- site_settings:
- discourse_ai_enabled: "Abilita il plugin Discourse AI."
- ai_toxicity_enabled: "Abilita il modulo tossicità."
- ai_toxicity_inference_service_api_endpoint: "URL in cui l'API è in esecuzione per il modulo tossicità"
- ai_toxicity_inference_service_api_key: "Chiave API per l'API di tossicità"
- ai_toxicity_inference_service_api_model: "Modello da utilizzare per l'inferenza. Il modello multilingue funziona con italiano, francese, russo, portoghese, spagnolo e turco."
- ai_toxicity_flag_automatically: "Contrassegna automaticamente post/messaggi di chat al di sopra delle soglie configurate."
- ai_toxicity_flag_threshold_toxicity: "Tossicità: un commento maleducato, irrispettoso o irragionevole che potrebbe farti abbandonare una discussione o rinunciare a condividere il tuo punto di vista"
- ai_toxicity_flag_threshold_severe_toxicity: "Grave tossicità: un commento molto odioso, aggressivo o irrispettoso che molto probabilmente ti farà abbandonare una discussione o rinunciare a condividere la tua prospettiva"
- ai_toxicity_flag_threshold_obscene: "Osceno"
- ai_toxicity_flag_threshold_identity_attack: "Attacco all'identità"
- ai_toxicity_flag_threshold_insult: "Insulto"
- ai_toxicity_flag_threshold_threat: "Minaccia"
- ai_toxicity_flag_threshold_sexual_explicit: "Sessualmente esplicito"
- ai_toxicity_groups_bypass: "Gli utenti di questi gruppi non vedranno i propri post classificati dal modulo sulla tossicità."
- ai_sentiment_enabled: "Abilita il modulo sentimento."
- ai_sentiment_inference_service_api_endpoint: "URL in cui l'API è in esecuzione per il modulo sentimento"
- ai_sentiment_inference_service_api_key: "Chiave API per l'API di sentimento"
- ai_sentiment_models: "Modelli da utilizzare per l'inferenza. Il sentimento classifica i messaggi nello spazio positivo/neutro/negativo. L'emozione si classifica nello spazio rabbia/disgusto/paura/gioia/neutrale/tristezza/sorpresa."
- ai_nsfw_detection_enabled: "Abilita il modulo NSFW."
- ai_nsfw_inference_service_api_endpoint: "URL in cui l'API è in esecuzione per il modulo NSFW"
- ai_nsfw_inference_service_api_key: "Chiave API per l'API NSFW"
- ai_nsfw_flag_automatically: "Contrassegna automaticamente i post NSFW che superano le soglie configurate."
- ai_nsfw_flag_threshold_general: "Soglia generale per un'immagine da considerare NSFW."
- ai_nsfw_flag_threshold_drawings: "Soglia per un disegno da considerare NSFW."
- ai_nsfw_flag_threshold_hentai: "Soglia per un'immagine classificata come hentai per essere considerata NSFW."
- ai_nsfw_flag_threshold_porn: "Soglia per un'immagine classificata come pornografica per essere considerata NSFW."
- ai_nsfw_flag_threshold_sexy: "Soglia per un'immagine classificata come sexy per essere considerata NSFW."
- ai_nsfw_models: "Modelli da utilizzare per l'inferenza NSFW."
- ai_helper_enabled: "Abilita l'assistente IA."
- composer_ai_helper_allowed_groups: "Gli utenti di questi gruppi vedranno il pulsante dell'assistente IA nella sezione di scrittura."
- ai_helper_allowed_in_pm: "Abilita l'assistente IA nei MP."
- ai_helper_model: "Modello da utilizzare per l'assistente IA."
- ai_helper_custom_prompts_allowed_groups: "Gli utenti di questi gruppi vedranno l'opzione di comando personalizzato nell'assistente IA."
- ai_helper_automatic_chat_thread_title_delay: "Ritardo in minuti prima che l'assistente IA imposti automaticamente il titolo del thread della chat."
- ai_helper_automatic_chat_thread_title: "Imposta automaticamente i titoli dei thread della chat in base ai contenuti del thread."
- ai_helper_illustrate_post_model: "Modello da utilizzare per la funzionalità di messaggio illustrato dell'assistente IA compositore"
- ai_helper_enabled_features: "Seleziona le funzionalità da abilitare nell'assistente IA."
- post_ai_helper_allowed_groups: "Gruppi di utenti autorizzati ad accedere alle funzionalità dell'assistente IA nei post"
- ai_helper_image_caption_model: "Seleziona il modello da utilizzare per generare le didascalie delle immagini"
- ai_auto_image_caption_allowed_groups: "Gli utenti di questi gruppi possono attivare/disattivare i sottotitoli automatici delle immagini."
- ai_embeddings_selected_model: "Usa il modello selezionato per generare le integrazioni."
- ai_embeddings_generate_for_pms: "Genera integrazioni per messaggi personali."
- ai_embeddings_semantic_related_topics_enabled: "Usa la ricerca semantica per argomenti correlati."
- ai_embeddings_semantic_related_topics: "Numero massimo di argomenti da mostrare nella sezione degli argomenti correlati."
- ai_embeddings_backfill_batch_size: "Numero di incorporamenti da riempire ogni 15 minuti."
- ai_embeddings_semantic_search_enabled: "Abilita la ricerca semantica a pagina intera."
- ai_embeddings_semantic_quick_search_enabled: "Abilita l'opzione di ricerca semantica nel popup del menu di ricerca."
- ai_embeddings_semantic_related_include_closed_topics: "Includi argomenti chiusi nei risultati della ricerca semantica"
- ai_embeddings_semantic_search_hyde_model: "Modello utilizzato per espandere le parole chiave per ottenere risultati migliori durante una ricerca semantica"
- ai_embeddings_per_post_enabled: Genera integrazioni per ogni post
- ai_summarization_model: "Modello da utilizzare per il riepilogo"
- ai_custom_summarization_allowed_groups: "I gruppi autorizzati a utilizzare la creazione di nuovi riepiloghi."
- ai_pm_summarization_allowed_groups: "Gruppi autorizzati a creare e visualizzare riepiloghi nei messaggi privati."
- ai_summary_gists_enabled: "Genera automaticamente brevi riepiloghi delle ultime risposte negli argomenti"
- ai_summary_gists_allowed_groups: "Gruppi autorizzati a visualizzare i concetti chiave nell'elenco degli argomenti più popolari."
- ai_summary_backfill_maximum_topics_per_hour: "Numero di riepiloghi degli argomenti da riempire all'ora."
- ai_bot_enabled: "Abilita il modulo Bot IA."
- ai_bot_enable_chat_warning: "Visualizza un avviso quando viene avviata la chat MP. Può essere sovrascritto modificando la stringa di traduzione: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Quando il bot GPT ha accesso a un MP, risponderà ai membri di questi gruppi."
- ai_bot_debugging_allowed_groups: "Consenti a questi gruppi di vedere un pulsante di debug sui post che mostra la richiesta e la risposta IA non elaborate"
- ai_bot_public_sharing_allowed_groups: "Consenti a questi gruppi di condividere messaggi personali dell'IA con il pubblico tramite un link univoco disponibile al pubblico. Nota: se il tuo sito richiede l'accesso, anche le condivisioni richiederanno l'accesso."
- ai_bot_add_to_header: "Mostra un pulsante nell'intestazione per avviare un MP con un Bot IA"
- ai_bot_github_access_token: "Token di accesso GitHub da utilizzare con gli strumenti GitHub IA (richiesto per il supporto della ricerca)"
- ai_stability_api_key: "Chiave API per l'API stability.ai"
- ai_stability_engine: "Sistema di generazione di immagini da utilizzare per l'API stability.ai"
- ai_stability_api_url: "URL per l'API stability.ai"
- ai_google_custom_search_api_key: "Chiave API per l'API della ricerca personalizzata di Google: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX per l'API di ricerca personalizzata di Google"
- reviewables:
- reasons:
- flagged_by_toxicity: Il plugin IA lo ha segnalato dopo averlo classificato come tossico.
- flagged_by_nsfw: Il plugin IA lo ha segnalato dopo aver classificato almeno una delle immagini allegate come NSFW.
- reports:
- overall_sentiment:
- title: "Sentimento generale"
- description: 'Il grafico confronta il numero di messaggi classificati come positivi o negativi. Questi vengono calcolati quando i punteggi positivi o negativi > il punteggio soglia impostato. Ciò significa che i messaggi neutri non vengono mostrati. Sono esclusi anche i messaggi personali (MP). Classificazione effettuata con "cardiffnlp/twitter-roberta-base-sentiment-latest"'
- xaxis: "Positiva(%)"
- yaxis: "Data"
- emotion_admiration:
- title: "\U0001F929 Ammirazione"
- description: "Messaggi classificati con l'emozione ammirazione tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_amusement:
- title: "\U0001F604 Divertimento"
- description: "Messaggi classificati con l'emozione divertimento tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_anger:
- title: "\U0001F620 Rabbia"
- description: "Messaggi classificati con l'emozione rabbia tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_annoyance:
- title: "\U0001F612 Fastidio"
- description: "Messaggi classificati con l'emozione fastidio tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_approval:
- title: "\U0001F44D Approvazione"
- description: "Messaggi classificati con l'emozione approvazione tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_caring:
- title: "\U0001F917 Premuroso"
- description: "Messaggi classificati con l'emozione premuroso tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_confusion:
- title: "\U0001F615 Confusione"
- description: "Messaggi classificati con l'emozione confusione tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_curiosity:
- title: "\U0001F914 Curiosità"
- description: "Messaggi classificati con l'emozione curiosità tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_desire:
- title: "\U0001F60D Desiderio"
- description: "Messaggi classificati con l'emozione desiderio tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_disappointment:
- title: "\U0001F61E Delusione"
- description: "Messaggi classificati con l'emozione delusione tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_disapproval:
- title: "\U0001F44E Disapprovazione"
- description: "Messaggi classificati con l'emozione disapprovazione tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_disgust:
- title: "\U0001F922 Disgusto"
- description: "Messaggi classificati con l'emozione disgusto tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_embarrassment:
- title: "\U0001F633 Imbarazzo"
- description: "Messaggi classificati con l'emozione imbarazzo tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_excitement:
- title: "\U0001F92A Eccitazione"
- description: "Messaggi classificati con l'emozione eccitazione tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_fear:
- title: "\U0001F628 Paura"
- description: "Messaggi classificati con l'emozione paura tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_gratitude:
- title: "\U0001F64F Gratitudine"
- description: "Messaggi classificati con l'emozione gratitudine tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_grief:
- title: "\U0001F622 Dolore"
- description: "Messaggi classificati con l'emozione dolore tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_joy:
- title: "\U0001F60A Gioia"
- description: "Messaggi classificati con l'emozione gioia tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_love:
- title: '❤️ Amore'
- description: "Messaggi classificati con l'emozione amore tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_nervousness:
- title: "\U0001F630 Nervosismo"
- description: "Messaggi classificati con l'emozione nervosismo tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_neutral:
- title: "\U0001F610 Neutrale"
- description: "Messaggi classificati con l'emozione neutrale tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_optimism:
- title: "\U0001F31F Ottimismo"
- description: "Messaggi classificati con l'emozione ottimismo tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_pride:
- title: "\U0001F981 Orgoglio"
- description: "Messaggi classificati con l'emozione orgoglio tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_realization:
- title: "\U0001F4A1 Realizzazione"
- description: "Messaggi classificati con l'emozione realizzazione tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_relief:
- title: "\U0001F60C Sollievo"
- description: "Messaggi classificati con l'emozione sollievo tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_remorse:
- title: "\U0001F614 Rimorso"
- description: "Messaggi classificati con l'emozione rimorso tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_sadness:
- title: "\U0001F62D Tristezza"
- description: "Messaggi classificati con l'emozione tristezza tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- emotion_surprise:
- title: "\U0001F632 Sorpresa"
- description: "Messaggi classificati con l'emozione sorpresa tramite intelligenza artificiale, utilizzando il modello 'SamLowe/roberta-base-go_emotions'."
- discourse_ai:
- ai_artifact:
- view_source: "Visualizza origine"
- view_changes: "Visualizza modifiche"
- unknown_model: "Modello IA sconosciuto"
- tools:
- custom_name: "%{name} (personalizzato)"
- presets:
- browse_web_jina:
- name: "Naviga sul web (jina.ai)"
- exchange_rate:
- name: "Tasso di cambio"
- stock_quote:
- name: "Quotazione azionaria (AlphaVantage)"
- image_generation:
- name: "Generatore di immagini di flusso (Together.ai)"
- empty_tool:
- name: "Inizia da zero..."
- ai_helper:
- errors:
- completion_request_failed: "Qualcosa è andato storto durante il tentativo di fornire suggerimenti. Riprova."
- prompts:
- translate: Traduci in %{language}
- generate_titles: Suggerisci i titoli degli argomenti
- proofread: Testo corretto
- markdown_table: Genera tabella di markdown
- custom_prompt: "Comando personalizzato"
- explain: "Spiega"
- illustrate_post: "Illustra messaggio"
- replace_dates: "Date smart"
- painter:
- attribution:
- stable_diffusion_xl: "Immagine di Stable Diffusion XL"
- dall_e_3: "Immagine di DALL-E 3"
- image_caption:
- attribution: "Didascalia da IA"
- share_ai:
- read_more: "Leggi la trascrizione completa"
- onebox_title: "Conversazione IA con %{llm_name}"
- formatted_excerpt: "Conversazione IA con %{llm_name}:\n %{excerpt}"
- title: "%{title} - Conversazione IA - %{site_name}"
- errors:
- not_allowed: "Non hai l'autorizzazione a condividere questo argomento"
- other_people_in_pm: "I messaggi personali con altri esseri umani non possono essere condivisi pubblicamente"
- other_content_in_pm: "I messaggi personali contenenti post di altre persone non possono essere condivisi pubblicamente"
- failed_to_share: "Condivisione della conversazione non riuscita"
- conversation_deleted: "Condivisione di conversazione eliminata correttamente"
- spam_detection:
- flag_reason: "Segnalato come spam da Discourse AI"
- silence_reason: "Utente silenziato automaticamente da Discourse AI"
- invalid_error_type: "Tipo di errore non valido fornito"
- unexpected: "Si è verificato un errore imprevisto"
- bot_user_update_failed: "Impossibile aggiornare l'utente del bot di scansione antispam"
- ai_bot:
- reply_error: "Spiacenti, il nostro sistema ha riscontrato un problema imprevisto durante il tentativo di risposta.\n\n[details='Dettagli errore']\n%{details}\n[/details]"
- default_pm_prefix: "[Bot IA senza titolo MP]"
- personas:
- default_llm_required: "Il modello LLM predefinito è obbligatorio prima di abilitare la chat"
- cannot_delete_system_persona: "I personaggi di sistema non possono essere eliminati, vanno invece disattivati"
- cannot_edit_system_persona: "I personaggi di sistema possono solo essere rinominati, non è possibile modificare strumenti di sistema, piuttosto disabilitali e crearne una copia"
- github_helper:
- name: "Assistente GitHub"
- description: "Bot IA specializzato nell'assistenza con attività e domande relative a GitHub"
- general:
- name: Assistente del forum
- description: "Bot IA per scopi generici in grado di eseguire vari compiti"
- artist:
- name: Artista
- description: "Bot IA specializzato nella generazione di immagini"
- sql_helper:
- name: Assistente SQL
- description: "Bot IA specializzato nell'aiutare a creare query SQL su questa istanza di Discourse"
- settings_explorer:
- name: Esploratore di impostazioni
- description: "Bot IA specializzato nell'aiutare a esplorare le impostazioni del sito Discourse"
- creative:
- name: Creativo
- description: "Bot IA senza integrazioni esterne specializzato in attività creative"
- dall_e3:
- name: "DALL-E 3"
- description: "Bot IA specializzato nella generazione di immagini tramite DALL-E 3"
- discourse_helper:
- name: "Assistente di Discourse"
- description: "Bot IA specializzato nell'aiutare con attività relative a Discourse"
- web_artifact_creator:
- name: "Creatore di artefatti web"
- description: "Bot IA specializzato nella creazione di artefatti web interattivi"
- custom_prompt:
- name: "Comando personalizzato"
- smart_dates:
- name: "Date smart"
- topic_not_found: "Riepilogo non disponibile, argomento non trovato!"
- summarizing: "Riepilogo argomento"
- searching: "Ricerca di: '%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "Numero massimo di risultati"
- google:
- base_query:
- name: "Query di ricerca di base"
- description: "Query di base da usare durante la ricerca. Esempi: 'site:example.com' includerà solo i risultati di example.com, before:2022-01-01 includerà solo i risultati del 2021 e precedenti. Questo testo è anteposto alla query di ricerca."
- read:
- read_private:
- name: "Leggi privato"
- description: "Consenti l'accesso a tutti gli argomenti a cui l'utente ha accesso (per impostazione predefinita sono inclusi solo gli argomenti pubblici)"
- search:
- search_private:
- name: "Cerca privato"
- description: "Includi tutti gli argomenti a cui l'utente ha accesso nei risultati di ricerca (per impostazione predefinita sono inclusi solo gli argomenti pubblici)"
- max_results:
- name: "Numero massimo di risultati"
- description: "Numero massimo di risultati da includere nella ricerca: se vuoto verranno utilizzate le regole predefinite e il conteggio verrà scalato in base al modello utilizzato. Il valore più alto è 100."
- base_query:
- name: "Query di ricerca di base"
- description: "Query di base da utilizzare durante la ricerca. Esempio: \"#urgente\" anteporrà \"#urgente\" alla query di ricerca e includerà solo gli argomenti con la categoria o l'etichetta urgente."
- tool_summary:
- update_artifact: "Aggiorna un artefatto web"
- create_artifact: "Crea artefatto web"
- web_browser: "Naviga sul web"
- github_search_files: "File di ricerca GitHub"
- github_search_code: "Ricerca codice GitHub"
- github_file_content: "Contenuto del file GitHub"
- github_pull_request_diff: "Diff richiesta pull GitHub"
- random_picker: "Selettore casuale"
- categories: "Elenca le categorie"
- search: "Cerca"
- tags: "Elenca le etichette"
- time: "Ora"
- summarize: "Riassumi"
- image: "Genera immagine"
- google: "Cerca su Google"
- read: "Leggi l'argomento"
- setting_context: "Cerca il contesto delle impostazioni del sito"
- schema: "Cerca lo schema del database"
- search_settings: "Ricerca nelle impostazioni del sito"
- dall_e: "Genera immagine"
- search_meta_discourse: "Cerca Meta Discourse"
- javascript_evaluator: "Valuta JavaScript"
- tool_help:
- update_artifact: "Aggiorna un artefatto web utilizzando il bot IA"
- create_artifact: "Crea un artefatto web utilizzando il bot IA"
- web_browser: "Sfoglia la pagina web utilizzando il Bot IA"
- github_search_code: "Cerca il codice in un repository GitHub"
- github_search_files: "Cerca file in un repository GitHub"
- github_file_content: "Recupera il contenuto dei file da un repository GitHub"
- github_pull_request_diff: "Recupera un diff della richiesta pull GitHub"
- random_picker: "Scegli un numero casuale o un elemento casuale di una lista"
- categories: "Elenca tutte le categorie visibili pubblicamente sul forum"
- search: "Cerca tutti gli argomenti pubblici sul forum"
- tags: "Elenca tutte le etichette sul forum"
- time: "Trova l'ora in diversi fusi orari"
- summary: "Riassumi un argomento"
- image: "Genera un'immagine utilizzando la diffusione stabile"
- google: "Cerca una query su Google"
- read: "Leggi l'argomento pubblico sul forum"
- setting_context: "Cerca il contesto delle impostazioni del sito"
- schema: "Cerca lo schema del database"
- search_settings: "Cerca le impostazioni del sito"
- dall_e: "Genera immagine utilizzando DALL-E 3"
- search_meta_discourse: "Cerca Meta Discourse"
- javascript_evaluator: "Valuta JavaScript"
- tool_description:
- update_artifact: "Aggiornato un artefatto web utilizzando il bot IA"
- web_browser: "Lettura %{url}"
- github_search_files: "Hai cercato \"%{keywords}\" in %{repo}/%{branch}"
- github_search_code: "Hai cercato \"%{query}\" in %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "Contenuto recuperato di %{file_paths} da %{repo_name}@%{branch}"
- random_picker: "Scelta da %{options}, scelto: %{result}"
- read: "Lettura: %{title}"
- time: "L'orario in %{timezone} è %{time}"
- summarize: "Riassunto di %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "%{count} categoria trovata"
- other: "%{count} categorie trovate"
- tags:
- one: "%{count} etichetta trovata"
- other: "%{count} etichette trovate"
- search:
- one: "Trovato %{count} risultato per '%{query}'"
- other: "Trovati %{count} risultati per '%{query}'"
- search_meta_discourse:
- one: "Trovato %{count} risultato per '%{query}'"
- other: "Trovati %{count} risultati per '%{query}'"
- google:
- one: "Trovato %{count} risultato per '%{query}'"
- other: "Trovati %{count} risultati per '%{query}'"
- setting_context: "Contesto di lettura per: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "Trovato %{count} risultato per '%{query}'"
- other: "Trovati %{count} risultati per '%{query}'"
- summarization:
- configuration_hint:
- one: "Configura prima l'impostazione \"%{setting}\"."
- other: "Configura prima queste impostazioni: %{settings}"
- chat:
- no_targets: "Non ci sono stati messaggi durante il periodo selezionato."
- sentiment:
- reports:
- overall_sentiment: "Sentimento generale (positivo - negativo)"
- post_emotion:
- sadness: "Tristezza \U0001F622"
- surprise: "Sorpresa \U0001F631"
- neutral: "Neutro \U0001F610"
- fear: "Paura \U0001F628"
- anger: "Rabbia \U0001F621"
- joy: "Gioia \U0001F600"
- disgust: "Disgusto \U0001F922"
- sentiment_analysis:
- positive: "Positiva"
- negative: "Negativa"
- neutral: "Neutro"
- llm:
- configuration:
- disable_module_first: "Devi prima disabilitare %{setting}."
- set_llm_first: "Imposta prima %{setting}"
- model_unreachable: "Non è stato possibile ottenere una risposta da questo modello. Controlla prima le tue impostazioni."
- invalid_seeded_model: "Non è possibile utilizzare questo modello con questa funzionalità"
- must_select_model: "Devi prima selezionare un LLM"
- endpoints:
- not_configured: "%{display_name} (non configurato)"
- configuration_hint:
- one: "Assicurati che l'impostazione `%{settings}` sia stata configurata."
- other: "Assicurati che queste impostazioni siano state configurate: %{settings}"
- delete_failed:
- one: "Non è stato possibile eliminare questo modello perché è utilizzato da %{settings}. Aggiorna l'impostazione e riprova."
- other: "Non è stato possibile eliminare questo modello perché lo stanno utilizzando %{settings}. Aggiorna le impostazioni e riprova."
- cannot_edit_builtin: "Non è possibile modificare un modello incorporato."
- embeddings:
- delete_failed: "Questo modello è attualmente in uso. Aggiorna prima `ai embeddings selected model`."
- cannot_edit_builtin: "Non è possibile modificare un modello incorporato."
- configuration:
- disable_embeddings: "Devi prima disabilitare \"integrazioni ia abilitate\"."
- choose_model: "Imposta prima \"ai embeddings selected model\"."
- llm_models:
- missing_provider_param: "%{param} non può essere vuoto"
- bedrock_invalid_url: "Compila tutti i campi per utilizzare questo modello."
- ai_staff_action_logger:
- updated: "aggiornato"
- removed: "rimosso"
- errors:
- quota_exceeded: "Hai superato la quota per questo modello. Riprova tra %{relative_time}."
- quota_required: "È necessario specificare il numero massimo di token o utilizzi per questo modello"
- no_query_specified: Il parametro di query è obbligatorio, specificalo.
- no_user_for_persona: Il personaggio specificato non ha alcun utente associato.
- persona_not_found: Il personaggio specificato non esiste. Controlla i parametri persona_name o persona_id.
- no_user_specified: Il parametro username o user_unique_id è obbligatorio, specificalo.
- user_not_found: L'utente specificato non esiste. Controlla il parametro username.
- persona_disabled: Il personaggio specificato è disattivato. Controlla i parametri persona_name o persona_id.
- no_default_llm: Il personaggio deve avere un default_llm definito.
- user_not_allowed: All'utente non è consentito partecipare all'argomento.
- prompt_message_length: Il messaggio %{idx} supera il limite di 1000 caratteri.
diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml
deleted file mode 100644
index 7096c4b0..00000000
--- a/config/locales/server.ja.yml
+++ /dev/null
@@ -1,434 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ja:
- discourse_automation:
- ai:
- flag_types:
- review: "レビューキューに投稿を追加"
- spam: "迷惑として通報して投稿を非表示"
- spam_silence: "迷惑として通報し、投稿を非表示にしてユーザーを投稿禁止にする"
- scriptables:
- llm_triage:
- title: AI を使った投稿のトリアージ
- description: "大規模言語モデルを使って投稿に優先順位を付けます"
- flagged_post: |
-
- Getriggerd door de regel %%AUTOMATION_NAME%%.
- llm_report:
- title: Periodiek rapport met AI
- description: "Periodiek rapport op basis van een groot taalmodel"
- site_settings:
- discourse_ai_enabled: "Schakel de Discours-AI-plug-in in."
- ai_toxicity_enabled: "Schakel de toxiciteitsmodule in."
- ai_toxicity_inference_service_api_endpoint: "URL waar de API wordt uitgevoerd voor de toxiciteitsmodule"
- ai_toxicity_inference_service_api_key: "API-sleutel voor de toxiciteits-API"
- ai_toxicity_inference_service_api_model: "Te gebruiken model voor inferentie. Het meertalige model werkt met Italiaans, Frans, Russisch, Portugees, Spaans en Turks."
- ai_toxicity_flag_automatically: "Markeer automatisch berichten/chatberichten die boven de geconfigureerde drempels komen."
- ai_toxicity_flag_threshold_toxicity: "Toxiciteit: een onbeschofte, respectloze of onredelijke opmerking die er enigszins waarschijnlijk toe leidt dat je een discussie verlaat of stopt met het delen van je mening"
- ai_toxicity_flag_threshold_severe_toxicity: "Ernstige toxiciteit: een zeer hatelijke, agressieve of respectloze opmerking die er zeer waarschijnlijk toe leidt dat je een discussie verlaat of stopt met het delen van je mening"
- ai_toxicity_flag_threshold_obscene: "Obsceen"
- ai_toxicity_flag_threshold_identity_attack: "Aanval op identiteit"
- ai_toxicity_flag_threshold_insult: "Belediging"
- ai_toxicity_flag_threshold_threat: "Bedreiging"
- ai_toxicity_flag_threshold_sexual_explicit: "Seksueel expliciet"
- ai_toxicity_groups_bypass: "De berichten van gebruikers in die groepen worden niet geclassificeerd door de toxiciteitsmodule."
- ai_sentiment_enabled: "Schakel de sentimentmodule in."
- ai_sentiment_inference_service_api_endpoint: "URL waar de API wordt uitgevoerd voor de sentimentmodule"
- ai_sentiment_inference_service_api_key: "API-sleutel voor de sentiment-API"
- ai_sentiment_models: "Te gebruiken model voor inferentie. Sentiment classificeert berichten in de ruimte positief/neutraal/negatief. Emotie classificeert berichten in de ruimte boosheid/afschuw/angst/vreugde/neutraal/verdriet/verrassing."
- ai_nsfw_detection_enabled: "Schakel de NVWW-module in."
- ai_nsfw_inference_service_api_endpoint: "URL waar de API wordt uitgevoerd voor de NVWW-module"
- ai_nsfw_inference_service_api_key: "API-sleutel voor de NVWW-API"
- ai_nsfw_flag_automatically: "Markeer automatisch NVWW-berichten die boven de geconfigureerde drempels komen."
- ai_nsfw_flag_threshold_general: "Algemene drempel voor een afbeelding om als NSFW te worden beschouwd."
- ai_nsfw_flag_threshold_drawings: "Drempel voor een tekening om als NSFW te worden beschouwd."
- ai_nsfw_flag_threshold_hentai: "Drempelwaarde voor een als hentai geclassificeerde afbeelding om als NSFW te worden beschouwd."
- ai_nsfw_flag_threshold_porn: "Drempelwaarde voor een als porno geclassificeerde afbeelding om als NSFW te worden beschouwd."
- ai_nsfw_flag_threshold_sexy: "Drempelwaarde voor een als sexy geclassificeerde afbeelding om als NSFW te worden beschouwd."
- ai_nsfw_models: "Te gebruiken modellen voor NVWW-inferentie."
- ai_helper_enabled: "Schakel de AI-helper in."
- composer_ai_helper_allowed_groups: "Gebruikers in deze groepen zien de AI-hulpknop in de editor."
- ai_helper_allowed_in_pm: "Schakel de AI-hulp van de editor in in PB's."
- ai_helper_model: "Te gebruiken model voor de AI-hulp."
- ai_helper_custom_prompts_allowed_groups: "Gebruikers in deze groepen zien de optie voor een aangepaste prompt in de AI-helper."
- ai_helper_automatic_chat_thread_title_delay: "Vertraging in minuten voordat de AI-helper automatisch de titel van de chatthread instelt."
- ai_helper_automatic_chat_thread_title: "Stel automatisch de titel van chatthreads in op basis van de threadinhoud."
- ai_helper_illustrate_post_model: "Te gebruiken model voor de berichtillustratiefunctie van de opstellings-AI-helper"
- ai_helper_enabled_features: "Selecteer de functies die je wilt inschakelen in de AI-helper."
- post_ai_helper_allowed_groups: "Gebruikersgroepen die toegang hebben tot AI-helperfuncties in berichten"
- ai_helper_image_caption_model: "Selecteer het te gebruiken model voor het genereren van afbeeldingsbijschriften"
- ai_auto_image_caption_allowed_groups: "Gebruikers in deze groepen kunnen automatische afbeeldingsbijschriften in- en uitschakelen."
- ai_embeddings_selected_model: "Gebruik het geselecteerde model voor het genereren van insluitingen."
- ai_embeddings_generate_for_pms: "Genereer insluitingen voor persoonlijke berichten."
- ai_embeddings_semantic_related_topics_enabled: "Gebruik semantisch zoeken voor gerelateerde topics."
- ai_embeddings_semantic_related_topics: "Maximaal aantal topics om weer te geven in de sectie met gerelateerde topics."
- ai_embeddings_backfill_batch_size: "Aantal insluitingen dat elke 15 minuten moet worden aangevuld."
- ai_embeddings_semantic_search_enabled: "Schakel semantisch zoeken op volledige pagina's in."
- ai_embeddings_semantic_quick_search_enabled: "Schakel de semantische zoekoptie in de zoekmenupop-up in."
- ai_embeddings_semantic_related_include_closed_topics: "Neem gesloten topics op in semantische zoekresultaten"
- ai_embeddings_semantic_search_hyde_model: "Gebruikt model voor het uitbreiden van trefwoorden om betere resultaten te krijgen bij semantisch zoeken"
- ai_embeddings_per_post_enabled: Insluitingen genereren voor elk bericht
- ai_summarization_model: "Te gebruiken model voor samenvattingen"
- ai_custom_summarization_allowed_groups: "Groepen die nieuwe samenvattingen mogen maken."
- ai_pm_summarization_allowed_groups: "Groepen mogen samenvattingen maken en bekijken in PB's."
- ai_summary_gists_enabled: "Genereer automatisch korte samenvattingen van de nieuwste reacties in topics"
- ai_summary_gists_allowed_groups: "Groepen kunnen samenvattingen zien in de lijst van populaire topics."
- ai_summary_backfill_maximum_topics_per_hour: "Aantal topicsamenvattingen om aan te vullen per uur."
- ai_bot_enabled: "Schakel de AI-botmodule in."
- ai_bot_enable_chat_warning: "Geef een waarschuwing weer wanneer een privéchat wordt gestart. Kan worden genegeerd door de vertaling te bewerken: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Wanneer de GPT-bot toegang heeft tot het privébericht, zal deze reageren op leden van deze groepen."
- ai_bot_debugging_allowed_groups: "Geef deze groepen een debugknop bij berichten die het ruwe AI-verzoek en -antwoord weergeeft"
- ai_bot_public_sharing_allowed_groups: "Sta deze groepen toe persoonlijke AI-berichten met het publiek delen via een unieke, openbaar beschikbare link. Let op: als aanmelden vereist is op je site, is dat ook vereist voor delen."
- ai_bot_add_to_header: "Geef een knop weer in de kop om een privéchat te starten met een AI-bot"
- ai_bot_github_access_token: "GitHub-toegangstoken voor gebruik met GitHub AI-tools (vereist voor zoekondersteuning)"
- ai_stability_api_key: "API-sleutel voor de stability.ai-API"
- ai_stability_engine: "Te gebruiken engine voor het genereren van afbeeldingen voor de stability.ai-API"
- ai_stability_api_url: "URL voor de stability.ai-API"
- ai_google_custom_search_api_key: "API-sleutel voor de Google Custom Search-API zie: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX voor Google Custom Search-API"
- reviewables:
- reasons:
- flagged_by_toxicity: De AI-plug-in heeft dit gemarkeerd nadat het als toxisch was geclassificeerd.
- flagged_by_nsfw: De AI-plug-in heeft dit gemarkeerd na classificatie van ten minste één van de bijgevoegde afbeeldingen als NVWW.
- reports:
- overall_sentiment:
- title: "Algemeen sentiment"
- description: 'De grafiek vergelijkt het aantal berichten dat als positief of negatief is geclassificeerd. Deze worden berekend wanneer positieve of negatieve scores groter zijn dan de ingestelde drempelscore. Dit betekent dat neutrale berichten niet worden weergegeven. Persoonlijke berichten (PB''s) zijn ook uitgesloten. Geclassificeerd met "cardiffnlp/twitter-roberta-base-sentiment-latest".'
- xaxis: "Positief (%)"
- yaxis: "Datum"
- emotion_admiration:
- title: "\U0001F929 Bewondering"
- description: "Berichten geclassificeerd met de emotie bewondering via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_amusement:
- title: "\U0001F604 Geamuseerd"
- description: "Berichten geclassificeerd met de emotie geamuseerd via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_anger:
- title: "\U0001F620 Boosheid"
- description: "Berichten geclassificeerd met de emotie woede via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_annoyance:
- title: "\U0001F612 Ergernis"
- description: "Berichten geclassificeerd met de emotie ergernis via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_approval:
- title: "\U0001F44D Goedkeuring"
- description: "Berichten geclassificeerd met de emotie goedkeuring via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_caring:
- title: "\U0001F917 Zorgzaam"
- description: "Berichten geclassificeerd met de emotie zorgzaam via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_confusion:
- title: "\U0001F615 Verwarring"
- description: "Berichten geclassificeerd met de emotie verwarring via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_curiosity:
- title: "\U0001F914 Nieuwsgierigheid"
- description: "Berichten geclassificeerd met de emotie nieuwsgierigheid via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_desire:
- title: "\U0001F60D Verlangen"
- description: "Berichten geclassificeerd met de emotie verlangen via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_disappointment:
- title: "\U0001F61E Teleurstelling"
- description: "Berichten geclassificeerd met de emotie teleurstelling via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_disapproval:
- title: "\U0001F44E Afkeuring"
- description: "Berichten geclassificeerd met de emotie afkeuring via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_disgust:
- title: "\U0001F922 Afschuw"
- description: "Berichten geclassificeerd met de emotie afschuw via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_embarrassment:
- title: "\U0001F633 Schaamte"
- description: "Berichten geclassificeerd met de emotie schaamte via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_excitement:
- title: "\U0001F92A Opwinding"
- description: "Berichten geclassificeerd met de emotie opwinding via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_fear:
- title: "\U0001F628 Angst"
- description: "Berichten geclassificeerd met de emotie angst via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_gratitude:
- title: "\U0001F64F Dankbaarheid"
- description: "Berichten geclassificeerd met de emotie dankbaarheid via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_grief:
- title: "\U0001F622 Verdriet"
- description: "Berichten geclassificeerd met de emotie verdriet via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_joy:
- title: "\U0001F60A Vreugde"
- description: "Berichten geclassificeerd met de emotie vreugde via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_love:
- title: '❤️ Liefde'
- description: "Berichten geclassificeerd met de emotie liefde via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_nervousness:
- title: "\U0001F630 Nervositeit"
- description: "Berichten geclassificeerd met de emotie nervositeit via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_neutral:
- title: "\U0001F610 Neutraal"
- description: "Berichten geclassificeerd met de emotie neutraal via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_optimism:
- title: "\U0001F31F Optimisme"
- description: "Berichten geclassificeerd met de emotie optimisme via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_pride:
- title: "\U0001F981 Trots"
- description: "Berichten geclassificeerd met de emotie trots via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_realization:
- title: "\U0001F4A1 Besef"
- description: "Berichten geclassificeerd met de emotie besef via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_relief:
- title: "\U0001F60C Opluchting"
- description: "Berichten geclassificeerd met de emotie oplichting via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_remorse:
- title: "\U0001F614 Spijt"
- description: "Berichten geclassificeerd met de emotie spijt via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_sadness:
- title: "\U0001F62D Droefheid"
- description: "Berichten geclassificeerd met de emotie droefheid via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- emotion_surprise:
- title: "\U0001F632 Verrassing"
- description: "Berichten geclassificeerd met de emotie verrassing via AI, met behulp van het model 'SamLowe/roberta-base-go_emotions'."
- discourse_ai:
- ai_artifact:
- view_source: "Bron weergeven"
- view_changes: "Wijzigingen weergeven"
- unknown_model: "Onbekend AI-model"
- tools:
- custom_name: "%{name} (aangepast)"
- presets:
- browse_web_jina:
- name: "Web surfen (jina.ai)"
- exchange_rate:
- name: "Wisselkoers"
- stock_quote:
- name: "Aandelenkoers (AlphaVantage)"
- image_generation:
- name: "Flux-afbeeldingsgenerator (Together.ai)"
- empty_tool:
- name: "Vanaf blanco beginnen..."
- ai_helper:
- errors:
- completion_request_failed: "Er is iets misgegaan bij het geven van suggesties. Probeer het opnieuw."
- prompts:
- translate: Vertalen naar %{language}
- generate_titles: Topictitels voorstellen
- proofread: Tekst proeflezen
- markdown_table: Markdowntabel genereren
- custom_prompt: "Aangepaste prompt"
- explain: "Uitleggen"
- illustrate_post: "Bericht illustreren"
- replace_dates: "Slimme datums"
- painter:
- attribution:
- stable_diffusion_xl: "Afbeelding door Stable Diffusion XL"
- dall_e_3: "Afbeelding door DALL-E 3"
- image_caption:
- attribution: "Bijschrift door AI"
- share_ai:
- read_more: "Volledig transcript lezen"
- onebox_title: "AI-conversatie met %{llm_name}"
- formatted_excerpt: "AI-conversatie met %{llm_name}:\n%{excerpt}"
- title: "%{title} - AI-conversatie - %{site_name}"
- errors:
- not_allowed: "Je mag dit topic niet delen"
- other_people_in_pm: "Persoonlijke berichten met andere mensen kunnen niet openbaar worden gedeeld"
- other_content_in_pm: "Persoonlijke berichten met berichten van andere mensen kunnen niet openbaar worden gedeeld"
- failed_to_share: "Delen van de conversatie mislukt"
- conversation_deleted: "Conversatiedeling verwijderd"
- spam_detection:
- flag_reason: "Gemarkeerd als spam door Discourse-AI"
- silence_reason: "Gebruiker automatisch gedempt door Discourse-AI"
- invalid_error_type: "Ongeldig fouttype opgegeven"
- unexpected: "Er is een onverwachte fout opgetreden"
- bot_user_update_failed: "Bijwerken van gebruiker voor spamscanbot mislukt"
- ai_bot:
- reply_error: "Het lijkt erop dat ons systeem een onverwacht probleem heeft ondervonden terwijl het probeerde te antwoorden.\n\n[details='Error details']\n%{details}\n[/details]"
- default_pm_prefix: "[Ongetitelde PB van AI-bot]"
- personas:
- default_llm_required: "Standaard LLM-model is vereist voordat Chat wordt ingeschakeld"
- cannot_delete_system_persona: "Systeempersona's kunnen niet worden verwijderd. Schakel deze in plaats daarvan uit"
- cannot_edit_system_persona: "Systeempersona's kunnen alleen worden hernoemd. Je kunt tools of systeemprompts niet bewerken. Je kunt ze wel uitschakelen en een kopie maken"
- github_helper:
- name: "GitHub Helper"
- description: "AI-bot gespecialiseerd in het assisteren bij GitHub-gerelateerde taken en vragen"
- general:
- name: Forumhelper
- description: "AI-bot voor algemeen gebruik die verschillende taken kan uitvoeren"
- artist:
- name: Artiest
- description: "AI-bot gespecialiseerd in het genereren van afbeeldingen"
- sql_helper:
- name: SQL-helper
- description: "AI-bot gespecialiseerd in het helpen opstellen van SQL-query's in dit Discourse-exemplaar"
- settings_explorer:
- name: Instellingenverkenner
- description: "AI-bot gespecialiseerd in het verkennen van Discourse-site-instellingen"
- creative:
- name: Creatief
- description: "AI-bot zonder externe integraties, gespecialiseerd in creatieve taken"
- dall_e3:
- name: "DALL-E 3"
- description: "AI-bot gespecialiseerd in het genereren van afbeeldingen met DALL-E 3"
- discourse_helper:
- name: "Discourse Helper"
- description: "AI-bot gespecialiseerd in het helpen bij Discourse-gerelateerde taken"
- web_artifact_creator:
- name: "Webartefactmaker"
- description: "AI-bot gespecialiseerd in het maken van interactieve webartefacten"
- custom_prompt:
- name: "Aangepaste prompt"
- smart_dates:
- name: "Slimme datums"
- topic_not_found: "Samenvatting niet beschikbaar, topic niet gevonden!"
- summarizing: "Topic samenvatten"
- searching: "Zoeken naar: '%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "Maximaal aantal resultaten"
- google:
- base_query:
- name: "Basiszoekquery"
- description: "Basisquery om te gebruiken bij het zoeken. Voorbeelden: 'site:example.com' levert alleen resultaten op van example.com, before:2022-01-01 levert alleen resultaten op van 2021 en eerder. Deze tekst wordt toegevoegd voorafgaand aan de zoekopdracht."
- read:
- read_private:
- name: "Privé lezen"
- description: "Sta toegang toe tot alle topics waartoe de gebruiker toegang heeft (standaard worden alleen openbare topics opgenomen)"
- search:
- search_private:
- name: "Privé zoeken"
- description: "Neem alle topics waartoe de gebruiker toegang heeft op in de zoekresultaten (standaard worden alleen openbare topics opgenomen)"
- max_results:
- name: "Maximaal aantal resultaten"
- description: "Maximaal aantal resultaten dat in de zoekopdracht moet worden opgenomen. Indien leeg worden er lege standaardregels gebruikt en wordt het aantal geschaald, afhankelijk van het gebruikte model. De hoogste waarde is 100."
- base_query:
- name: "Basiszoekquery"
- description: "Basisquery om te gebruiken bij het zoeken. Voorbeeld: '#urgent' voegt '#urgent' toe aan de zoekquery en neemt alleen topics mee met de categorie of tag 'urgent'."
- tool_summary:
- update_artifact: "Werk een webartefact bij"
- create_artifact: "Webartefact maken"
- web_browser: "Web surfen"
- github_search_files: "GitHub-zoekbestanden"
- github_search_code: "GitHub-code zoeken"
- github_file_content: "GitHub-bestandsinhoud"
- github_pull_request_diff: "GitHub pullverzoek-diff"
- random_picker: "Willekeurige kiezer"
- categories: "Categorieën weergeven"
- search: "Zoeken"
- tags: "Tags weergeven"
- time: "Tijd"
- summarize: "Samenvatten"
- image: "Afbeelding genereren"
- google: "Zoeken op Google"
- read: "Topic lezen"
- setting_context: "Site-instellingscontext opzoeken"
- schema: "Databaseschema opzoeken"
- search_settings: "Zoeken in site-instellingen"
- dall_e: "Afbeelding genereren"
- search_meta_discourse: "Zoeken in Meta Discourse"
- javascript_evaluator: "JavaScript evalueren"
- tool_help:
- update_artifact: "Werk een webartefact bij met behulp van de AI-bot"
- create_artifact: "Maak een webartefact met behulp van de AI-bot"
- web_browser: "Webpagina bekijken met behulp van de AI-bot"
- github_search_code: "Zoek code in een GitHub-repository"
- github_search_files: "Zoek bestanden in een GitHub-repository"
- github_file_content: "Haal de inhoud van bestanden op uit een GitHub-repository"
- github_pull_request_diff: "Haal een GitHub pullverzoek-diff op"
- random_picker: "Kies een willekeurig getal of een willekeurig element uit een lijst"
- categories: "Geef een lijst weer van alle openbaar zichtbare categorieën op het forum"
- search: "Doorzoek alle openbare topics op het forum"
- tags: "Geef een lijst weer van alle tags op het forum"
- time: "Zoek de tijd in verschillende tijdzones"
- summary: "Vat een topic samen"
- image: "Afbeelding genereren met Stable Diffusion"
- google: "Query zoeken op Google"
- read: "Openbaar topic op forum lezen"
- setting_context: "Site-instellingscontext opzoeken"
- schema: "Databaseschema opzoeken"
- search_settings: "Site-instellingen zoeken"
- dall_e: "Afbeelding genereren met DALL-E 3"
- search_meta_discourse: "Zoeken in Meta Discourse"
- javascript_evaluator: "JavaScript evalueren"
- tool_description:
- update_artifact: "Webartefact bijgewerkt met behulp van de AI-bot"
- web_browser: "Lezen: %{url}"
- github_search_files: "Gezocht naar '%{keywords}' in %{repo}/%{branch}"
- github_search_code: "Gezocht naar '%{query}' in %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "Inhoud van %{file_paths} opgehaald uit %{repo_name}@%{branch}"
- random_picker: "Kiezen uit %{options}, gekozen: %{result}"
- read: "Lezen: %{title}"
- time: "De tijd in %{timezone} is %{time}"
- summarize: "Samengevat: %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "%{count} categorie gevonden"
- other: "%{count} categorieën gevonden"
- tags:
- one: "%{count} tag gevonden"
- other: "%{count} tags gevonden"
- search:
- one: "%{count} resultaat gevonden voor '%{query}'"
- other: "%{count} resultaten gevonden voor '%{query}'"
- search_meta_discourse:
- one: "%{count} resultaat gevonden voor '%{query}'"
- other: "%{count} resultaten gevonden voor '%{query}'"
- google:
- one: "%{count} resultaat gevonden voor '%{query}'"
- other: "%{count} resultaten gevonden voor '%{query}'"
- setting_context: "Context lezen voor: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "%{count} resultaat gevonden voor '%{query}'"
- other: "%{count} resultaten gevonden voor '%{query}'"
- summarization:
- configuration_hint:
- one: "Configureer eerst de instelling '%{setting}'."
- other: "Configureer eerst deze instellingen: '%{settings}'."
- chat:
- no_targets: "Er waren geen berichten tijdens de geselecteerde periode."
- sentiment:
- reports:
- overall_sentiment: "Algeheel sentiment (positief - negatief)"
- post_emotion:
- sadness: "Droefheid \U0001F622"
- surprise: "Verrassing \U0001F631"
- neutral: "Neutraal \U0001F610"
- fear: "Angst \U0001F628"
- anger: "Boosheid \U0001F621"
- joy: "Vreugde \U0001F600"
- disgust: "Afschuw \U0001F922"
- sentiment_analysis:
- positive: "Positief"
- negative: "Negatief"
- neutral: "Neutraal"
- llm:
- configuration:
- disable_module_first: "Je moet eerst %{setting} uitschakelen."
- set_llm_first: "Stel eerst %{setting} in"
- model_unreachable: "We konden geen antwoord krijgen van dit model. Controleer eerst je instellingen."
- invalid_seeded_model: "Je kunt dit model niet gebruiken met deze functie"
- must_select_model: "Je moet eerst een LLM selecteren"
- endpoints:
- not_configured: "%{display_name} (niet geconfigureerd)"
- configuration_hint:
- one: "Zorg dat de instelling `%{settings}` is geconfigureerd."
- other: "Zorg dat deze instellingen zijn geconfigureerd: %{settings}"
- delete_failed:
- one: "We kunnen dit model niet verwijderen omdat %{settings} het gebruikt. Werk de instelling bij en probeer het opnieuw."
- other: "We kunnen dit model niet verwijderen omdat %{settings} het gebruiken. Werk de instellingen bij en probeer het opnieuw."
- cannot_edit_builtin: "Je kunt een ingebouwd model niet bewerken."
- embeddings:
- delete_failed: "Dit model is momenteel in gebruik. Werk eerst 'ai embeddings selected model' bij."
- cannot_edit_builtin: "Je kunt een ingebouwd model niet bewerken."
- configuration:
- disable_embeddings: "Je moet 'ai embeddings enabled' eerst uitschakelen."
- choose_model: "Stel eerst 'ai embeddings selected model' in."
- llm_models:
- missing_provider_param: "%{param} mag niet leeg zijn"
- bedrock_invalid_url: "Vul alle velden in om dit model te gebruiken."
- ai_staff_action_logger:
- updated: "bijgewerkt"
- removed: "verwijderd"
- errors:
- quota_exceeded: "Je hebt het quotum voor dit model overschreden. Probeer het opnieuw over %{relative_time}."
- quota_required: "Je moet het maximale aantal tokens of gebruiken opgeven voor dit model"
- no_query_specified: De queryparameter is verplicht. Geef deze op.
- no_user_for_persona: Er is geen gebruiker gekoppeld aan de opgegeven persona.
- persona_not_found: De opgegeven persona bestaat niet. Controleer de parameters persona_name en persona_id.
- no_user_specified: De gebruikersnaam of de parameter user_unique_id is vereist. Geef deze op.
- user_not_found: De opgegeven gebruiker bestaat niet. Controleer de gebruikersnaamparameter.
- persona_disabled: De opgegeven persona is uitgeschakeld. Controleer de parameters persona_name en persona_id.
- no_default_llm: Er moet een default_llm zijn gedefinieerd voor de persona.
- user_not_allowed: De gebruiker mag niet deelnemen aan het topic.
- prompt_message_length: Het bericht %{idx} is langer dan de limiet van 1000 tekens.
diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml
deleted file mode 100644
index a18b339a..00000000
--- a/config/locales/server.pl_PL.yml
+++ /dev/null
@@ -1,286 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-pl_PL:
- discourse_automation:
- ai:
- flag_types:
- spam: "Oznacz jako spam i ukryj post"
- spam_silence: "Oznacz jako spam, ukryj post i wycisz użytkownika"
- scriptables:
- llm_triage:
- title: Oceniaj posty za pomocą sztucznej inteligencji
- description: "Oceniaj posty przy użyciu dużego modelu językowego"
- llm_report:
- title: Raport okresowy z wykorzystaniem AI
- description: "Okresowy raport oparty na dużym modelu językowym"
- site_settings:
- discourse_ai_enabled: "Włącz wtyczkę Discourse AI."
- ai_toxicity_enabled: "Włącz moduł toksyczności."
- ai_toxicity_inference_service_api_endpoint: "Adres URL, pod którym działa interfejs API modułu toksyczności"
- ai_toxicity_inference_service_api_key: "Klucz dla API toksyczności"
- ai_toxicity_inference_service_api_model: "Model używany do wnioskowania. Model wielojęzyczny współpracuje z włoskim, francuskim, rosyjskim, portugalskim, hiszpańskim i tureckim."
- ai_toxicity_flag_automatically: "Automatycznie oflaguj posty / wiadomości na czacie, które przekraczają skonfigurowane progi."
- ai_toxicity_flag_threshold_toxicity: "Toksyczność: niegrzeczny, lekceważący lub nierozsądny komentarz, który może sprawić, że opuścisz dyskusję lub zrezygnujesz z dzielenia się swoją perspektywą"
- ai_toxicity_flag_threshold_severe_toxicity: "Poważna toksyczność: bardzo nienawistny, agresywny lub lekceważący komentarz, który najprawdopodobniej sprawi, że opuścisz dyskusję lub zrezygnujesz z dzielenia się swoją perspektywą"
- ai_toxicity_flag_threshold_obscene: "Nieprzyzwoity"
- ai_toxicity_flag_threshold_identity_attack: "Atak na tożsamość"
- ai_toxicity_flag_threshold_insult: "Znieważenie"
- ai_toxicity_flag_threshold_threat: "Zagrożenie"
- ai_toxicity_flag_threshold_sexual_explicit: "Treści erotyczne"
- ai_toxicity_groups_bypass: "Posty użytkowników w tych grupach nie będą klasyfikowane według modułu toksyczności."
- ai_sentiment_enabled: "Włącz moduł sentymentu."
- ai_sentiment_inference_service_api_endpoint: "Adres URL, pod którym działa interfejs API dla modułu sentymentu"
- ai_sentiment_inference_service_api_key: "Klucz dla API sentymentu"
- ai_sentiment_models: "Modele używane do wnioskowania. Sentyment klasyfikuje post w przestrzeni pozytywnej/neutralnej/negatywnej. Emocje klasyfikuje się w przestrzeni gniewu/wstrętu/strachu/radości/neutralności/smutku/zaskoczenia."
- ai_nsfw_detection_enabled: "Włącz moduł NSFW."
- ai_nsfw_inference_service_api_endpoint: "Adres URL, pod którym działa interfejs API dla modułu NSFW"
- ai_nsfw_inference_service_api_key: "Klucz dla NSFW API"
- ai_nsfw_flag_automatically: "Automatycznie oflaguj posty NSFW, które przekraczają skonfigurowane progi."
- ai_nsfw_flag_threshold_general: "Ogólny próg dla obrazu, który ma zostać uznany za NSFW."
- ai_nsfw_flag_threshold_drawings: "Próg uznania rysunku za NSFW."
- ai_nsfw_flag_threshold_hentai: "Próg uznania obrazu zaklasyfikowanego jako hentai, aby został uznany za NSFW."
- ai_nsfw_flag_threshold_porn: "Próg uznania obrazu zaklasyfikowanego jako pornograficzny, aby został uznany za NSFW."
- ai_nsfw_flag_threshold_sexy: "Próg uznania obrazu zaklasyfikowanego jako seksowny, aby został uznany za NSFW."
- ai_nsfw_models: "Modele używane do wnioskowania NSFW."
- composer_ai_helper_allowed_groups: "Użytkownicy w tych grupach zobaczą przycisk pomocy AI w kompozytorze."
- ai_helper_allowed_in_pm: "Włącz asystenta AI kompozytora w PW."
- ai_helper_model: "Model do użycia dla pomocnika AI."
- ai_helper_custom_prompts_allowed_groups: "Użytkownicy w tych grupach zobaczą opcję niestandardowego monitu w pomocniku AI."
- ai_helper_automatic_chat_thread_title_delay: "Opóźnienie w minutach, zanim pomocnik AI automatycznie ustawi tytuł wątku czatu."
- ai_helper_automatic_chat_thread_title: "Automatycznie ustawiaj tytuły wątków czatu na podstawie zawartości wątku."
- ai_embeddings_generate_for_pms: "Generuj osadzenia dla wiadomości osobistych."
- ai_embeddings_semantic_related_topics_enabled: "Użyj wyszukiwania semantycznego dla powiązanych tematów."
- ai_embeddings_semantic_related_topics: "Maksymalna liczba tematów do wyświetlenia w sekcji powiązanych tematów."
- ai_embeddings_backfill_batch_size: "Liczba osadzań do uzupełnienia co 15 minut."
- ai_embeddings_semantic_search_enabled: "Włącz wyszukiwanie semantyczne na całej stronie."
- ai_embeddings_semantic_related_include_closed_topics: "Uwzględnij zamknięte tematy w semantycznych wynikach wyszukiwania"
- ai_embeddings_semantic_search_hyde_model: "Model używany do rozwijania słów kluczowych w celu uzyskania lepszych wyników podczas wyszukiwania semantycznego"
- ai_summarization_model: "Model używany do podsumowania"
- ai_custom_summarization_allowed_groups: "Grupy, które mogą tworzyć nowe podsumowania."
- ai_pm_summarization_allowed_groups: "Grupy mogą tworzyć i przeglądać podsumowania w wiadomościach prywatnych."
- ai_summary_gists_enabled: "Automatycznie generuj krótkie podsumowania najnowszych odpowiedzi w tematach"
- ai_summary_gists_allowed_groups: "Grupy mogą przeglądać sedno dyskusji na liście gorących tematów."
- ai_summary_backfill_maximum_topics_per_hour: "Liczba podsumowań tematów do uzupełnienia na godzinę."
- ai_bot_enabled: "Włącz moduł bota AI."
- ai_bot_enable_chat_warning: "Wyświetl ostrzeżenie po zainicjowaniu czatu PW. Można je zastąpić, edytując ciąg tłumaczenia: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Gdy bot GPT uzyska dostęp do PW, odpowie członkom tych grup."
- ai_bot_add_to_header: "Wyświetl przycisk w nagłówku, aby rozpocząć PW z botem AI"
- ai_stability_api_key: "Klucz dla API stability.ai"
- ai_stability_engine: "Silnik generowania obrazów do wykorzystania w stability.ai API"
- ai_stability_api_url: "Adres URL interfejsu API stability.ai"
- ai_google_custom_search_api_key: "Klucz dla Google Custom Search API, zobacz: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX dla Google Custom Search API"
- reviewables:
- reasons:
- flagged_by_toxicity: Wtyczka AI oznaczyła to po sklasyfikowaniu jako toksyczne.
- flagged_by_nsfw: Wtyczka AI oznaczyła to po sklasyfikowaniu co najmniej jednego z załączonych obrazów jako NSFW.
- reports:
- overall_sentiment:
- title: "Ogólny sentyment"
- xaxis: "Pozytywny(%)"
- yaxis: "Data"
- emotion_anger:
- title: "\U0001F620 Gniew"
- emotion_disgust:
- title: "\U0001F922 Niesmak"
- emotion_fear:
- title: "\U0001F628 Strach"
- emotion_neutral:
- title: "\U0001F610 Neutralne"
- emotion_sadness:
- title: "\U0001F62D Smutek"
- emotion_surprise:
- title: "\U0001F632 Niespodzianka"
- discourse_ai:
- ai_artifact:
- view_source: "Zobacz źródło"
- view_changes: "Zobacz zmiany"
- change_description: "Zmień opis"
- unknown_model: "Nieznany model AI"
- ai_helper:
- errors:
- completion_request_failed: "Coś poszło nie tak podczas próby przedstawienia sugestii. Spróbuj ponownie."
- prompts:
- translate: Przetłumacz na %{language}
- generate_titles: Zaproponuj tytuły tematu
- proofread: Popraw tekst
- markdown_table: Wygeneruj tabelę Markdown
- custom_prompt: "Niestandardowy monit"
- explain: "Wyjaśnij"
- illustrate_post: "Zilustruj post"
- share_ai:
- read_more: "Przeczytaj pełną transkrypcję"
- errors:
- not_allowed: "Nie masz uprawnień do udostępniania tego tematu"
- failed_to_share: "Nie udało się udostępnić konwersacji"
- conversation_deleted: "Udostępniona konwersacja została pomyślnie usunięta"
- spam_detection:
- flag_reason: "Oznaczono jako spam przez Discourse AI"
- silence_reason: "Użytkownik wyciszony automatycznie przez Discourse AI"
- invalid_error_type: "Podano nieprawidłowy typ błędu"
- unexpected: "Wystąpił nieoczekiwany błąd"
- bot_user_update_failed: "Nie udało się zaktualizować użytkownika bota skanującego spam"
- ai_bot:
- default_pm_prefix: "[PW bota AI bez tytułu]"
- thinking: "Myślenie..."
- personas:
- cannot_delete_system_persona: "Person systemowych nie można usunąć, zamiast tego wyłącz je."
- general:
- name: Pomocnik forumowy
- description: "Bot AI ogólnego przeznaczenia zdolny do wykonywania różnych zadań"
- artist:
- name: Artysta
- description: "Bot AI specjalizujący się w generowaniu obrazów"
- sql_helper:
- name: Pomocnik SQL
- description: "Bot AI specjalizujący się w pomaganiu w tworzeniu zapytań SQL na tej instancji Discourse."
- settings_explorer:
- name: Eksplorator ustawień
- description: "Bot AI specjalizujący się w eksploracji ustawień strony Discourse"
- creative:
- name: Kreatywny
- description: "Bot AI bez zewnętrznych integracji specjalizujący się w zadaniach kreatywnych"
- dall_e3:
- name: "DALL-E 3"
- description: "Bot AI specjalizujący się w generowaniu obrazów przy użyciu DALL-E 3"
- short_summarizer:
- name: "Podsumowanie (forma krótka)"
- custom_prompt:
- name: "Niestandardowy prompt"
- topic_not_found: "Podsumowanie niedostępne, nie znaleziono tematu!"
- summarizing: "Podsumowanie tematu"
- searching: "Wyszukiwanie: '%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "Maksymalna liczba wyników"
- update_artifact:
- update_algorithm:
- description: "Poproś LLM o pełne zastąpienie lub użyj diff do aktualizacji"
- google:
- base_query:
- name: "Podstawowe zapytanie wyszukiwania"
- search:
- max_results:
- name: "Maksymalna liczba wyników"
- description: "Maksymalna liczba wyników do uwzględnienia w wyszukiwaniu - jeśli zostaną użyte puste reguły domyślne, liczba zostanie przeskalowana w zależności od użytego modelu. Najwyższa wartość to 100."
- base_query:
- name: "Podstawowe zapytanie wyszukiwania"
- description: "Podstawowe zapytanie używane podczas wyszukiwania. Przykład: '#pilne' spowoduje dodanie '#pilne' do zapytania wyszukiwania i uwzględnienie tylko tematów z kategorią lub tagiem pilne."
- tool_summary:
- web_browser: "Przeglądaj sieć"
- random_picker: "Losowy selektor"
- categories: "Wymień kategorie"
- search: "Szukaj"
- tags: "Wymień tagi"
- time: "Czas"
- summarize: "Podsumuj"
- image: "Wygeneruj obraz"
- google: "Szukaj w Google"
- read: "Przeczytaj temat"
- setting_context: "Wyszukaj kontekst ustawienia witryny"
- schema: "Wyszukaj schemat bazy danych"
- search_settings: "Wyszukiwanie ustawień witryny"
- dall_e: "Wygeneruj obraz"
- tool_help:
- random_picker: "Wybierz losową liczbę lub losowy element listy"
- categories: "Wyświetl wszystkie publicznie widoczne kategorie na forum"
- search: "Przeszukaj wszystkie publiczne tematy na forum"
- tags: "Wymień wszystkie tagi na forum"
- time: "Znajdź czas w różnych strefach czasowych"
- summary: "Podsumuj temat"
- image: "Wygeneruj obraz przy użyciu Stable Diffusion"
- google: "Wyszukaj zapytanie w Google"
- read: "Przeczytaj temat publiczny na forum"
- setting_context: "Wyszukaj kontekst ustawienia witryny"
- schema: "Wyszukaj schemat bazy danych"
- search_settings: "Wyszukaj ustawienia witryny"
- dall_e: "Wygeneruj obraz za pomocą DALL-E 3"
- tool_description:
- random_picker: "Wybieranie z %{options}, wybrane: %{result}"
- read: "Czytanie: %{title}"
- time: "Czas w %{timezone} wynosi %{time}"
- summarize: "Podsumowano %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "Znaleziono %{count} kategorię"
- few: "Znaleziono %{count} kategorie"
- many: "Znaleziono %{count} kategorii"
- other: "Znaleziono %{count} kategorii"
- tags:
- one: "Znaleziono %{count} tag"
- few: "Znaleziono %{count} tagi"
- many: "Znaleziono %{count} tagów"
- other: "Znaleziono %{count} tagów"
- search:
- one: "Znaleziono %{count} wyników dla '%{query}'"
- few: "Znaleziono %{count} wyniki dla '%{query}'"
- many: "Znaleziono %{count} wyników dla '%{query}'"
- other: "Znaleziono %{count} wyników dla '%{query}'"
- search_meta_discourse:
- one: "Znaleziono %{count} wyników dla '%{query}'"
- few: "Znaleziono %{count} wyniki dla '%{query}'"
- many: "Znaleziono %{count} wyników dla '%{query}'"
- other: "Znaleziono %{count} wyników dla '%{query}'"
- google:
- one: "Znaleziono %{count} wyników dla '%{query}'"
- few: "Znaleziono %{count} wyniki dla '%{query}'"
- many: "Znaleziono %{count} wyników dla '%{query}'"
- other: "Znaleziono %{count} wyników dla '%{query}'"
- setting_context: "Kontekst czytania dla: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "Znaleziono %{count} wynik dla '%{query}'"
- few: "Znaleziono %{count} wyniki dla '%{query}'"
- many: "Znaleziono %{count} wyników dla '%{query}'"
- other: "Znaleziono %{count} wyników dla '%{query}'"
- summarization:
- configuration_hint:
- one: "Najpierw skonfiguruj ustawienie `%{setting}`."
- few: "Najpierw skonfiguruj ustawienia: %{settings}"
- many: "Najpierw skonfiguruj ustawienia: %{settings}"
- other: "Najpierw skonfiguruj ustawienia: %{settings}"
- chat:
- no_targets: "W wybranym okresie nie było żadnych wiadomości."
- sentiment:
- reports:
- post_emotion:
- sadness: "Smutek \U0001F622"
- surprise: "Niespodzianka \U0001F631"
- neutral: "Neutralne \U0001F610"
- fear: "Strach \U0001F628"
- anger: "Gniew \U0001F621"
- disgust: "Niesmak \U0001F922"
- sentiment_analysis:
- positive: "Pozytywne"
- negative: "Negatywne"
- neutral: "Neutralne"
- llm:
- configuration:
- disable_module_first: "Najpierw musisz wyłączyć %{setting}."
- set_llm_first: "Najpierw ustaw %{setting}"
- model_unreachable: "Nie mogliśmy uzyskać odpowiedzi od tego modelu. Sprawdź najpierw swoje ustawienia."
- embeddings:
- cannot_edit_builtin: "Nie możesz edytować wbudowanego modelu."
- configuration:
- invalid_config: "Wybrałeś nieprawidłową opcję."
- llm_models:
- missing_provider_param: "%{param} nie może być pusty"
- bedrock_invalid_url: "Wypełnij wszystkie pola, aby korzystać z tego modelu."
- ai_staff_action_logger:
- updated: "zaktualizowane"
- removed: "usunięto"
- errors:
- quota_exceeded: "Przekroczyłeś limit dla tego modelu. Spróbuj ponownie za %{relative_time}."
- quota_required: "Musisz określić maksymalną liczbę tokenów lub użyć dla tego modelu"
- no_query_specified: Parametr zapytania jest wymagany, podaj go.
- no_user_for_persona: Określona persona nie ma powiązanego użytkownika.
- persona_not_found: Określona persona nie istnieje. Sprawdź parametry persona_name lub persona_id.
- prompt_message_length: Wiadomość %{idx} przekracza limit 1000 znaków.
diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml
deleted file mode 100644
index be9bd2b7..00000000
--- a/config/locales/server.pt.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-pt:
- reports:
- overall_sentiment:
- yaxis: "Data"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Pesquisar"
- time: "Hora"
- summarize: "Resumir"
- ai_staff_action_logger:
- updated: "atualizado"
- removed: "removido"
diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml
deleted file mode 100644
index 75bd3c46..00000000
--- a/config/locales/server.pt_BR.yml
+++ /dev/null
@@ -1,444 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-pt_BR:
- discourse_automation:
- ai:
- flag_types:
- review: "Adicionar postagem à fila de revisão"
- spam: "Sinalizar como spam e ocultar postagem"
- spam_silence: "Sinalizar como spam, ocultar postagem e silenciar usuário(a)"
- scriptables:
- llm_triage:
- title: Postagens de triagem usando IA
- description: "Postagens de triagem usando um modelo de linguagem grande"
- flagged_post: |
-
Resposta do modelo:
-
%%LLM_RESPONSE%%
- Acionada pela regra %%AUTOMATION_NAME%%.
- llm_report:
- title: Relatório periódico usando IA
- description: "Relatório periódico baseado em um modelo de linguagem grande"
- site_settings:
- discourse_ai_enabled: "Ative o plugin de IA do Discourse."
- ai_toxicity_enabled: "Ative o módulo de toxicidade."
- ai_toxicity_inference_service_api_endpoint: "URL em que a API está em execução para o módulo de toxicidade"
- ai_toxicity_inference_service_api_key: "Chave de API para a API de toxicidade"
- ai_toxicity_inference_service_api_model: "Modelo usado para inferência. O modelo multilinguístico funciona com italiano, francês, russo, português, espanhol e turco."
- ai_toxicity_flag_automatically: "Sinalize automaticamente postagens/mensagens de chat que excedem os limites configurados."
- ai_toxicity_flag_threshold_toxicity: "Toxicidade: comentário grosseiro, desrespeitoso ou insensato com alguma probabilidade de causar a saída da discussão ou desistência de compartilhar opinião."
- ai_toxicity_flag_threshold_severe_toxicity: "Toxicidade grave: comentário muito desrespeitoso, agressivo ou odioso com alta probabilidade de causar a saída da discussão ou desistência de compartilhar opinião."
- ai_toxicity_flag_threshold_obscene: "Obsceno"
- ai_toxicity_flag_threshold_identity_attack: "Ataque à identidade"
- ai_toxicity_flag_threshold_insult: "Insulto"
- ai_toxicity_flag_threshold_threat: "Ameaça"
- ai_toxicity_flag_threshold_sexual_explicit: "Conteúdo sexual explícito"
- ai_toxicity_groups_bypass: "Os(as) usuários(as) não terão suas postagens classificadas pelo módulo de toxicidade."
- ai_sentiment_enabled: "Ative o módulo de sentimento."
- ai_sentiment_inference_service_api_endpoint: "URL em que a API está em execução para o módulo de sentimento"
- ai_sentiment_inference_service_api_key: "Chave de API para a API de sentimento"
- ai_sentiment_models: "Modelos usado para inferência. O sentimento classifica a postagem em positiva/neutra/negativa. A emoção classifica em ódio/repúdio/medo/alegria/neutro/tristeza/surpresa."
- ai_nsfw_detection_enabled: "Ative o módulo NSFW."
- ai_nsfw_inference_service_api_endpoint: "URL em que a API está em execução para o módulo NSFW"
- ai_nsfw_inference_service_api_key: "Chave de API para a API de NSFW"
- ai_nsfw_flag_automatically: "Sinalize automaticamente postagens de NSFW que excedem os limites configurados."
- ai_nsfw_flag_threshold_general: "Limite geral para uma imagem ser considerada NSFW."
- ai_nsfw_flag_threshold_drawings: "Limite para um desenho ser considerado NSFW."
- ai_nsfw_flag_threshold_hentai: "Limite para uma imagem classificada como hentai ser considerada NSFW."
- ai_nsfw_flag_threshold_porn: "Limite para uma imagem classificada como pornográfica ser considerada NSFW."
- ai_nsfw_flag_threshold_sexy: "Limite para uma imagem classificada como sensual ser considerada NSFW."
- ai_nsfw_models: "Modelos usados para inferência de NSFW."
- ai_helper_enabled: "Habilite o assistente de IA."
- composer_ai_helper_allowed_groups: "Os usuários desses grupos visualizarão o botão de assistência por IA no compositor."
- ai_helper_allowed_in_pm: "Ative o ajudante de IA do compositor em MPs."
- ai_helper_model: "Modelo usado para o ajudante de IA."
- ai_helper_custom_prompts_allowed_groups: "Os(as) usuários(as) que estiverem nestes grupos verão um prompt personalizado no ajudante da IA."
- ai_helper_automatic_chat_thread_title_delay: "O atraso, em minutos, até que o ajudante de IA defina automaticamente o título da thread do chat."
- ai_helper_automatic_chat_thread_title: "Defina automaticamente os títulos das linhas de discussão do chat com base no conteúdo delas."
- ai_helper_illustrate_post_model: "O modelo a ser usado para o ajudante de IA ilustrar o recurso da postagem"
- ai_helper_enabled_features: "Selecione os recursos para ativar no ajudante de IA."
- post_ai_helper_allowed_groups: "Grupos de usuários(as) com permissão para acessar recursos do ajudante de IA nas postagens"
- ai_helper_image_caption_model: "Selecione o modelo a ser usado para gerar legendas em imagens"
- ai_auto_image_caption_allowed_groups: "Usuários(as) nestes grupos podem alternar legendas automáticas em imagens."
- ai_embeddings_selected_model: "Use o modelo selecionado para gerar incorporações"
- ai_embeddings_generate_for_pms: "Gere incorporações para mensagens pessoais."
- ai_embeddings_semantic_related_topics_enabled: "Use Pesquisa semântica para tópicos relacionados."
- ai_embeddings_semantic_related_topics: "Número máximo de tópicos na seção do tópico relacionado."
- ai_embeddings_backfill_batch_size: "O número de incorporações para provisionamento a cada 15 minutos."
- ai_embeddings_semantic_search_enabled: "Ative pesquisa semântica em toda a página."
- ai_embeddings_semantic_quick_search_enabled: "Ative opção de busca semântica no pop-up no menu de busca."
- ai_embeddings_semantic_related_include_closed_topics: "Incluir tópicos fechados em resultados de pesquisa semântica"
- ai_embeddings_semantic_search_hyde_model: "O modelo usado para expandir palavras-chave e obter resultados melhores durante uma pesquisa semântica"
- ai_embeddings_per_post_enabled: Gerar incorporações para cada postagem
- ai_summarization_model: "Modelo a ser usado para geração de resumos"
- ai_custom_summarization_allowed_groups: "Grupos com permissão para a criação de resumos novos."
- ai_pm_summarization_allowed_groups: "Grupos autorizados a criar e visualizar resumos em PMs."
- ai_summary_gists_allowed_groups: "Grupos com permissão para ver gists na lista de tópicos mais acessados."
- ai_summary_backfill_maximum_topics_per_hour: "Quantidade de tópicos para preencher a cada hora."
- ai_bot_enabled: "Ative o módulo de Bot com IA."
- ai_bot_enable_chat_warning: "Exiba um aviso quando o chat por MP for iniciado. Pode ser substituído ao editar a string de tradução: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Quando o Bot com GPT tiver acesso às MPs, ele responderá aos membros destes grupos."
- ai_bot_debugging_allowed_groups: "Permitir a exibição de um botão de depuração nestes grupos, em postagens em que são exibidas respostas e pedidos de IA não processados"
- ai_bot_public_sharing_allowed_groups: "Permita que estes grupos compartilhem mensagens pessoais de IA com o público através de um link exclusivo disponível publicamente. Observação: se for preciso entrar com sua conta no site, também será preciso para o compartilhamento."
- ai_bot_add_to_header: "Exibir botão no cabeçalho para começar uma MP com um Bot de IA"
- ai_bot_github_access_token: "Token de acesso do GitHub a ser usado com ferramentas de IA do GitHub (necessário para ter compatibilidade com pesquisa)"
- ai_stability_api_key: "Chave de API para a API stability.ai"
- ai_stability_engine: "Mecanismo de geração de imagem usado para a API stability.ai"
- ai_stability_api_url: "URL para a API stability.ai"
- ai_google_custom_search_api_key: "Chave de API para a API de pesquisa personalizada do Google. Veja: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX para API de pesquisa personalizada do Google"
- reviewables:
- reasons:
- flagged_by_toxicity: O plugin de IA sinalizou isto após classificar como tóxico.
- flagged_by_nsfw: O plugin de IA sinalizou isto após classificar pelo menos uma das imagens anexas como NSFW.
- reports:
- overall_sentiment:
- title: "Sentimento geral"
- description: 'Este gráfico compara a quantidade de postagens classificadas como positiva ou negativa. São calculadas quando as pontuações negativas e positivas forem maiores que a pontuação de limite. Ou seja, postagens neutras não são exibidas. Mensagens pessoais (PM) também não. Classificado com "cardiffnlp/twitter-roberta-base-sentiment-latest"'
- xaxis: "Positivo (%)"
- yaxis: "Data"
- emotion_admiration:
- title: "\U0001F929 Admiração"
- description: "Postagens classificadas com o gesto de admiração pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_amusement:
- title: "\U0001F604 Diversão"
- description: "Postagens classificadas com o gesto de diversão pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_anger:
- title: "\U0001F620 Raiva"
- description: "Postagens classificadas com o gesto de raiva pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_annoyance:
- title: "\U0001F612 Aborrecimento"
- description: "Postagens classificadas com o gesto de aborrecimento pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_approval:
- title: "\U0001F44D Aprovação"
- description: "Postagens classificadas com o gesto de aprovação pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_caring:
- title: "\U0001F917 Carinho"
- description: "Postagens classificadas com o gesto de carinho pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_confusion:
- title: "\U0001F615 Confusão"
- description: "Postagens classificadas com o gesto de confusão pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_curiosity:
- title: "\U0001F914 Curiosidade"
- description: "Postagens classificadas com o gesto de curiosidade pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_desire:
- title: "\U0001F60D Desejo"
- description: "Postagens classificadas com o gesto de desejo pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_disappointment:
- title: "\U0001F61E Decepção"
- description: "Postagens classificadas com o gesto de decepção pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_disapproval:
- title: "\U0001F44E Desaprovação"
- description: "Postagens classificadas com o gesto de desaprovação pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_disgust:
- title: "\U0001F922 Repulsa"
- description: "Postagens classificadas com o gesto de desgosto pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_embarrassment:
- title: "\U0001F633 Vergonha"
- description: "Postagens classificadas com o gesto de vergonha pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_excitement:
- title: "\U0001F92A Empolgação"
- description: "Postagens classificadas com o gesto de empolgação pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_fear:
- title: "\U0001F628 Medo"
- description: "Postagens classificadas com o gesto de medo pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_gratitude:
- title: "\U0001F64F Gratidão"
- description: "Postagens classificadas com o gesto de gratidão pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_grief:
- title: "\U0001F622 Pesar"
- description: "Postagens classificadas com o gesto de pesar pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_joy:
- title: "\U0001F60A Alegria"
- description: "Postagens classificadas com o gesto de alegria pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_love:
- title: '❤️ Amor'
- description: "Postagens classificadas com o gesto de amor pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_nervousness:
- title: "\U0001F630 Nervosismo"
- description: "Postagens classificadas com o gesto de nervosismo pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_neutral:
- title: "\U0001F610 Neutro"
- description: "Postagens classificadas com o gesto de neutralidade pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_optimism:
- title: "\U0001F31F Otimismo"
- description: "Postagens classificadas com o gesto de otimismo pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_pride:
- title: "\U0001F981 Orgulho"
- description: "Postagens classificadas com o gesto de orgulho pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_realization:
- title: "\U0001F4A1 Percepção"
- description: "Postagens classificadas com o gesto de percepção pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_relief:
- title: "\U0001F60C Alívio"
- description: "Postagens classificadas com o gesto de alívio pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_remorse:
- title: "\U0001F614 Remorso"
- description: "Postagens classificadas com o gesto de remorso pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_sadness:
- title: "\U0001F62D Tristeza"
- description: "Postagens classificadas com o gesto de tristeza pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- emotion_surprise:
- title: "\U0001F632 Surpresa"
- description: "Postagens classificadas com o gesto de surpresa pela IA, usando o modelo \"SamLowe/roberta-base-go_emotions\"."
- discourse_ai:
- ai_artifact:
- view_source: "Ver origem"
- view_changes: "Visualizar alterações"
- unknown_model: "Modelo de IA desconhecido"
- tools:
- custom_name: "%{name} (personalizado)"
- presets:
- browse_web_jina:
- name: "Navegar na Web com (jina.ai)"
- exchange_rate:
- name: "Taxa de câmbio"
- stock_quote:
- name: "Cotação de ação (AlphaVantage)"
- image_generation:
- name: "Gerador de imagem de fluxo (Together.ai)"
- empty_tool:
- name: "Começar do zero..."
- ai_helper:
- errors:
- completion_request_failed: "Algo deu errado ao tentar dar sugestões. Tente novamente."
- prompts:
- translate: Traduzir para %{language}
- generate_titles: Sugerir títulos de tópicos
- proofread: Revisar texto
- markdown_table: Gerar tabela de Markdown
- custom_prompt: "Prompt personalizado"
- explain: "Explicar"
- illustrate_post: "Ilustrar postagem"
- replace_dates: "Datas inteligentes"
- painter:
- attribution:
- stable_diffusion_xl: "Imagem criada por Stable Diffusion XL"
- dall_e_3: "Imagem criada por DALL-E 3"
- image_caption:
- attribution: "Legendado por IA"
- share_ai:
- read_more: "Ler a transcrição completa"
- onebox_title: "Convesa de IA com %{llm_name}"
- formatted_excerpt: "Convesa de IA com %{llm_name}:\n %{excerpt}"
- title: "%{title} - Conversa de AI - %{site_name}"
- errors:
- not_allowed: "Você não tem permissão para compartilhar este tópico"
- other_people_in_pm: "Não é possível compartilhar publicamente mensagens pessoais com outros humanos"
- other_content_in_pm: "Não é possível compartilhar publicamente mensagens pessoais que contêm postagens de outras pessoas"
- failed_to_share: "Falha ao compartilhar conversa"
- conversation_deleted: "Compartilhamento de conversa excluído"
- spam_detection:
- flag_reason: "Sinalizada como spam pelo Discourse AI"
- silence_reason: "Usuário(a) silenciado(a) automaticamente pelo Discourse AI"
- invalid_error_type: "Tipo inválido de erro fornecido"
- unexpected: "Ocorreu um erro inesperado"
- bot_user_update_failed: "Falha ao atualizar usuário(a) do bot de verificação de spam"
- ai_bot:
- reply_error: "Desculpe, parece que nosso sistema encontrou um problema inesperado ao tentar responder.\n\n[details='Error details']\n%{details}\n[/details]"
- default_pm_prefix: "[MP de bot de IA não identificado]"
- personas:
- default_llm_required: "É preciso de um modelo de LLM padrão para ativar o chat"
- cannot_delete_system_persona: "Personas de sistema não podem ser excluídas, desative"
- cannot_edit_system_persona: "Personas de sistema só podem ser renomeadas, você não pode editar ferramentas ou prompt de sistema. Desative e faça uma cópia"
- github_helper:
- name: "Ajudante do GitHub"
- description: "Bot de IA especializado em auxiliar em tarefas e perguntas relacionadas ao GitHub"
- general:
- name: Ajudante do fórum
- description: "Bot de IA de propósito geral capaz de realizar várias tarefas"
- artist:
- name: Artista
- description: "Bot de IA especializado em gerar imagens"
- sql_helper:
- name: Ajudante SQL
- description: "Bot de IA especializado em ajudar a criar consultas SQL nesta instância do Discourse"
- settings_explorer:
- name: Explorador de configurações
- description: "Bot de IA especializado em ajudar a explorar as configurações do site do Discourse"
- creative:
- name: Criativa
- description: "Bot de IA sem integrações externas especializado em tarefas criativas"
- dall_e3:
- name: "DALL-E 3"
- description: "Bot de IA especializado em gerar imagens usando o DALL-E 3"
- discourse_helper:
- name: "Ajudante do Discourse"
- description: "Bot de IA especializado em ajudar em tarefas relacionadas ao Discourse"
- web_artifact_creator:
- name: "Criador de artefato da web"
- description: "Bot especializado na criação de artefatos interativos da web"
- custom_prompt:
- name: "Prompt personalizado"
- smart_dates:
- name: "Datas inteligentes"
- topic_not_found: "Resumo indisponível, tópico não encontrado!"
- summarizing: "Resumindo tópico"
- searching: "Pesquisando: \"%{query}\""
- tool_options:
- researcher:
- max_results:
- name: "O número máximo de resultados"
- google:
- base_query:
- name: "Consulta de pesquisa básica"
- description: "Consulta de base para usar em pesquisas. Por exemplo: \"site:example.com\" incluirá apenas resultados de example.com, before:2022-01-01 apenas resultados de 2021 em diante. Presume-se que este texto é a consulta de pesquisa."
- read:
- read_private:
- name: "Ler conteúdo privado"
- description: "Permitir acesso a todos os tópicos que o(a) usuário(a) pode acessar (por padrão, apenas tópicos públicos estão inclusos)"
- search:
- search_private:
- name: "Pesquisa de conteúdo privado"
- description: "Incluir todos os tópicos que o(a) usuário(a) pode acessar nos resultados de pesquisa (por padrão, apenas tópicos públicos estão inclusos)"
- max_results:
- name: "O número máximo de resultados"
- description: "A quantidade máxima de resultados para incluir na pesquisa. Se for vazio, serão usadas as regras padrão e a contagem será dimensionada conforme o modelo utilizado. O valor máximo é 100."
- base_query:
- name: "Consulta de pesquisa básica"
- description: "A consulta de base para usar na pesquisa. Exemplo: \"#urgent\" precederá \"#urgent\" para a consulta da pesquisa e incluirá apenas tópicos com a etiqueta ou categoria urgente."
- tool_summary:
- update_artifact: "Atualizar um artefato da Web"
- create_artifact: "Criar artefato da web"
- web_browser: "Navegar na Web"
- github_search_files: "Arquivos de pesquisa do GitHub"
- github_search_code: "Pesquisa de código do GitHub"
- github_file_content: "Conteúdo de arquivo do GitHub"
- github_pull_request_diff: "Diferença de solicitação pull do GitHub"
- random_picker: "Seletor aleatório"
- categories: "Listar categorias"
- search: "Pesquisar"
- tags: "Listar etiquetas"
- time: "Hora"
- summarize: "Resumir"
- image: "Gerar imagem"
- google: "Pesquisar no Google"
- read: "Ler tópico"
- setting_context: "Procurar contexto de configuração do site"
- schema: "Procurar esquema de banco de dados"
- search_settings: "Pesquisando configurações do site"
- dall_e: "Gerar imagem"
- search_meta_discourse: "Metapesquisa do Discourse"
- javascript_evaluator: "Avaliar JavaScript"
- tool_help:
- update_artifact: "Atualizar um artefato da Web que usa o Bot de IA"
- create_artifact: "Criar artefato da web usando o Bot de IA"
- web_browser: "Navegar na página da Web com bot de IA"
- github_search_code: "Procurar código no repositório do GitHub"
- github_search_files: "Procurar arquivos no repositório do GitHub"
- github_file_content: "Recuperar conteúdo de arquivos de um repositório do GitHub"
- github_pull_request_diff: "Recuperar diferença de solicitação pull do GitHub"
- random_picker: "Escolher um número ou elemento aleatório de uma lista"
- categories: "Listar todas as categorias visíveis publicamente no fórum"
- search: "Pesquisar todos os tópicos públicos no fórum"
- tags: "Listar todas as etiquetas no fórum"
- time: "Achar horário em vários fusos horários"
- summary: "Resumir um tópico"
- image: "Gerar imagem usando o Stable Diffusion"
- google: "Pesquisar uma consulta no Google"
- read: "Ler tópico público no fórum"
- setting_context: "Procurar contexto de configuração do site"
- schema: "Procurar esquema de banco de dados"
- search_settings: "Pesquisar configurações do site"
- dall_e: "Gerar imagem usando DALL-E 3"
- search_meta_discourse: "Metapesquisa do Discourse"
- javascript_evaluator: "Avaliar JavaScript"
- tool_description:
- update_artifact: "Artefato da Web que usa o Bot de IA atualizado"
- web_browser: "Lendo %{url}"
- github_search_files: "Pesquisa por \"%{keywords}\" em %{repo}/%{branch}"
- github_search_code: "Pesquisa por \"%{query}\" em %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "Conteúdo recuperado em %{file_paths} de %{repo_name}@%{branch}"
- random_picker: "Escolhendo %{options}, escolha: %{result}"
- read: "Lendo: %{title}"
- time: "A hora em %{timezone} é %{time}"
- summarize: "Resumo de %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "%{count} categoria encontrada"
- other: "%{count} categorias encontrada"
- tags:
- one: "%{count} etiqueta encontrada"
- other: "%{count} etiquetas encontradas"
- search:
- one: "%{count} resultado encontrado para \"%{query}\""
- other: "%{count} resultados encontrados para \"%{query}\""
- search_meta_discourse:
- one: "%{count} resultado encontrado para \"%{query}\""
- other: "%{count} resultados encontrados para \"%{query}\""
- google:
- one: "%{count} resultado encontrado para \"%{query}\""
- other: "%{count} resultados encontrados para \"%{query}\""
- setting_context: "Lendo contexto para: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "%{count} resultado encontrado para \"%{query}\""
- other: "%{count} resultados encontrados para \"%{query}\""
- summarization:
- configuration_hint:
- one: "Defina a configuração \"%{setting}\" primeiro."
- other: "Defina estas configurações primeiro: %{settings}"
- chat:
- no_targets: "Não houve mensagens durante o período selecionado."
- sentiment:
- reports:
- overall_sentiment: "Opinião geral (Positiva - Negativa)"
- post_emotion:
- sadness: "Tristeza \U0001F622"
- surprise: "Surpresa \U0001F631"
- neutral: "Neutro \U0001F610"
- fear: "Medo \U0001F628"
- anger: "Raiva \U0001F621"
- joy: "Alegria \U0001F600"
- disgust: "Repulsa \U0001F922"
- sentiment_analysis:
- positive: "Positivo"
- negative: "Negativo"
- neutral: "Neutro"
- llm:
- configuration:
- disable_module_first: "Você precisa desativar %{setting} primeiro."
- set_llm_first: "Defina %{setting} primeiro"
- model_unreachable: "Não foi possível obter uma resposta deste modelo. Confira as configurações primeiro."
- invalid_seeded_model: "Não é possível usar este modelo com este recurso"
- must_select_model: "Selecione um LLM primeiro"
- endpoints:
- not_configured: "%{display_name} (não configurado)"
- configuration_hint:
- one: "Verifique se a configuração \"%{settings}\" foi definida."
- other: "Verifique se estas configurações foram definidas: %{settings}"
- delete_failed:
- one: "Não foi possível excluir este modelo, pois está sendo usado por %{settings}. Atualize a configuração e tente novamente."
- other: "Não foi possível excluir este modelo, pois está sendo usado por %{settings}. Atualize a configuração e tente novamente."
- cannot_edit_builtin: "Não é possível eidtar um modelo integrado."
- embeddings:
- delete_failed: "Este modelo está em uso no momento. Atualize o modelo selecionado de incorporações de IA primeiro."
- cannot_edit_builtin: "Não é possível editar um modelo integrado."
- configuration:
- disable_embeddings: "Você precisa desativar \"incorporações com IA ativadas\" primeiro."
- choose_model: "Defina o \"modelo selecionado de incorporações com IA\" primeiro."
- llm_models:
- missing_provider_param: "%{param} Não pode ficar em branco"
- bedrock_invalid_url: "Preencha todos os campos para usar este modelo."
- ai_staff_action_logger:
- updated: "atualizado(a)"
- removed: "removido(a)"
- errors:
- quota_exceeded: "Você ultrapassou a cota deste modelo. Tente novamente em %{relative_time}."
- quota_required: "Você deve especificar o máximo de tokens ou usos para este modelo"
- no_query_specified: Requer parâmetro de consulta, especifique um.
- no_user_for_persona: A persona especificada não tem usuário(a) associado(a).
- persona_not_found: A persona especificada não existe. Confira os parâmetros persona_name ou persona_id
- no_user_specified: Requer o parâmetro username ou user_unique_id. Insira-o.
- user_not_found: O(a) usuário(a) especificado(a) não existe. Confira o parâmetro username.
- persona_disabled: A persona especificada está desativada. Confira os parâmetros persona_name ou persona_id
- no_default_llm: A persona deve ter default_llm definida.
- user_not_allowed: O(a) usuário(a) não tem permissão para participar do tópico.
- prompt_message_length: A mensagem %{idx} excede o limite de 1000 caracteres.
diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml
deleted file mode 100644
index d297baa1..00000000
--- a/config/locales/server.ro.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ro:
- reports:
- overall_sentiment:
- yaxis: "Dată"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Căutare"
- time: "Oră"
- summarize: "Rezumat"
- ai_staff_action_logger:
- updated: "actualizat"
diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml
deleted file mode 100644
index b74d824f..00000000
--- a/config/locales/server.ru.yml
+++ /dev/null
@@ -1,463 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ru:
- discourse_automation:
- ai:
- flag_types:
- review: "Добавить публикацию в очередь проверки"
- spam: "Пометить как спам и скрыть публикацию"
- spam_silence: "Пометить как спам, скрыть публикацию и заморозить пользователя"
- scriptables:
- llm_triage:
- title: Сортировка публикаций с помощью AI
- description: "Сортировка публикаций с использованием большой языковой модели"
- flagged_post: |
-
Ответ модели:
-
%%LLM_RESPONSE%%
- Срабатывает по правилу: %%AUTOMATION_NAME%%.
- llm_report:
- title: Периодический отчет с использованием AI
- description: "Периодический отчет на основе большой языковой модели"
- site_settings:
- discourse_ai_enabled: "Включить плагин AI для Discourse."
- ai_toxicity_enabled: "Включить модуль проверки на токсичность."
- ai_toxicity_inference_service_api_endpoint: "URL-адрес, по которому работает API для модуля проверки на токсичность"
- ai_toxicity_inference_service_api_key: "API-ключ для API проверки на токсичность"
- ai_toxicity_inference_service_api_model: "Модель, используемая для проверки. Многоязычная модель работает с итальянским, французским, русским, португальским, испанским и турецким языками."
- ai_toxicity_flag_automatically: "Автоматически жаловаться на сообщения (публичные и в чате), которые превышают указанные пороговые значения."
- ai_toxicity_flag_threshold_toxicity: "Токсичность: грубый, неуважительный или необоснованный комментарий, который может заставить вас покинуть обсуждение или удержаться от высказывания своей точки зрения."
- ai_toxicity_flag_threshold_severe_toxicity: "Высокая токсичность: очень злобный, агрессивный или неуважительный комментарий, который наверняка заставит вас покинуть обсуждение или удержаться от высказывания своей точки зрения."
- ai_toxicity_flag_threshold_obscene: "Непристойность"
- ai_toxicity_flag_threshold_identity_attack: "Атака на личность"
- ai_toxicity_flag_threshold_insult: "Оскорбление"
- ai_toxicity_flag_threshold_threat: "Угроза"
- ai_toxicity_flag_threshold_sexual_explicit: "Контент сексуального характера"
- ai_toxicity_groups_bypass: "Сообщения пользователей в этих группах не будут оцениваться модулем проверки на токсичность."
- ai_sentiment_enabled: "Включить модуль оценки настроения."
- ai_sentiment_inference_service_api_endpoint: "URL-адрес, по которому работает API для модуля оценки настроения"
- ai_sentiment_inference_service_api_key: "API-ключ для API оценки настроения"
- ai_sentiment_models: "Модели, используемые для оценки. Настроение определяется как позитивное, нейтральное или негативное. Эмоции распределяются по следующим категориям: гнев, отвращение, страх, радость, нейтральность, печаль, удивление."
- ai_nsfw_detection_enabled: "Включить модуль проверки на NSFW-контент."
- ai_nsfw_inference_service_api_endpoint: "URL-адрес, по которому работает API для модуля проверки на NSFW-контент"
- ai_nsfw_inference_service_api_key: "API-ключ для API проверки на NSFW-контент"
- ai_nsfw_flag_automatically: "Автоматически жаловаться на публичные сообщения с NSFW-контентом, которые превышают указанные пороговые значения."
- ai_nsfw_flag_threshold_general: "Общее пороговое значение, при котором изображение определяется как NSFW-контент."
- ai_nsfw_flag_threshold_drawings: "Пороговое значение, при котором рисунок определяется как NSFW-контент."
- ai_nsfw_flag_threshold_hentai: "Пороговое значение, при котором изображение, отнесенное к категории «хентай», определяется как NSFW-контент."
- ai_nsfw_flag_threshold_porn: "Пороговое значение, при котором изображение, отнесенное к категории «порно», определяется как NSFW-контент."
- ai_nsfw_flag_threshold_sexy: "Пороговое значение, при котором изображение, отнесенное к категории «сексуальное», определяется как NSFW-контент."
- ai_nsfw_models: "Модели, используемые для оценки NSFW-контента."
- ai_helper_enabled: "Включить AI-помощника"
- composer_ai_helper_allowed_groups: "Для пользователей в этих группах будет отображаться кнопка AI-помощника в редакторе."
- ai_helper_allowed_in_pm: "Включить AI-помощник для редактора в личных сообщениях."
- ai_helper_model: "Модели, используемые для AI-помощника."
- ai_helper_custom_prompts_allowed_groups: "Пользователи этих групп увидят опцию пользовательского запроса в AI- помощнике."
- ai_helper_automatic_chat_thread_title_delay: "Задержка в минутах, прежде чем AI-помощник автоматически установит заголовок темы чата."
- ai_helper_automatic_chat_thread_title: "Автоматически устанавливать заголовки тем чата в зависимости от содержимого темы."
- ai_helper_illustrate_post_model: "Модель, которая будет использоваться для функции иллюстрации публикации AI-помощника для редактора"
- ai_helper_enabled_features: "Выберите функции, которые нужно включить в AI-помощнике."
- post_ai_helper_allowed_groups: "Группы пользователей, которым разрешен доступ к функциям AI-помощника в публикациях"
- ai_helper_image_caption_model: "Выберите модель, которая будет использоваться для создания подписей к изображениям"
- ai_auto_image_caption_allowed_groups: "Пользователи в этих группах могут включать автоматическое добавление подписей к изображениям."
- ai_embeddings_selected_model: "Использовать выбранную модель для создания встраиваний."
- ai_embeddings_generate_for_pms: "Генерировать векторные представления для личных сообщений."
- ai_embeddings_semantic_related_topics_enabled: "Использовать семантический поиск для связанных тем."
- ai_embeddings_semantic_related_topics: "Максимальное количество тем для показа в разделе связанных тем."
- ai_embeddings_backfill_batch_size: "Количество векторных представлений для обратного заполнения каждые 15 минут."
- ai_embeddings_semantic_search_enabled: "Включить полностраничный семантический поиск."
- ai_embeddings_semantic_quick_search_enabled: "Включить опцию семантического поиска во всплывающем меню поиска."
- ai_embeddings_semantic_related_include_closed_topics: "Включать в результаты семантического поиска закрытые темы"
- ai_embeddings_semantic_search_hyde_model: "Модель, используемая для расширения ключевых слов, чтобы получить лучшие результаты во время семантического поиска"
- ai_embeddings_per_post_enabled: Генерировать эмбеддинги для каждой публикации
- ai_summarization_model: "Модель, используемая для формирования сводки"
- ai_custom_summarization_allowed_groups: "Группы, которым разрешено делать новые сводки."
- ai_pm_summarization_allowed_groups: "Группам разрешено создавать и просматривать сводки в личных сообщениях."
- ai_summary_gists_enabled: "Автоматически создает краткие сводки последних ответов в темах"
- ai_summary_gists_allowed_groups: "Группы, которые могут просматривать краткие сводки в списке горячих тем."
- ai_summary_backfill_maximum_topics_per_hour: "Число сводок по теме для заполнения за час."
- ai_bot_enabled: "Включить модуль AI-бота."
- ai_bot_enable_chat_warning: "Показывать предупреждение при запуске личного чата. Можно переопределить путем редактирования строки перевода: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Если у GPT-бота есть доступ к личным сообщениям, он будет отвечать участникам указанных групп."
- ai_bot_debugging_allowed_groups: "Разрешить этим группам видеть кнопку отладки в публикациях, которая отображает необработанный запрос и ответ AI."
- ai_bot_public_sharing_allowed_groups: "Разрешить этим группам делиться личными сообщениями AI с общественностью по уникальной общедоступной ссылке. Примечание: если ваш сайт требует входа, для доступа также потребуется вход."
- ai_bot_add_to_header: "Кнопка в заголовке для начала личного разговора с AI-ботом"
- ai_bot_github_access_token: "Токен доступа к GitHub для использования с инструментами GitHub AI (требуется для поддержки поиска)"
- ai_stability_api_key: "API-ключ для API stability.ai"
- ai_stability_engine: "Движок генерации изображений для использования в API stability.ai"
- ai_stability_api_url: "URL-адрес для API stability.ai"
- ai_google_custom_search_api_key: "API-ключ для API программируемого поиска Google — см. https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX для API программируемого поиска Google"
- reviewables:
- reasons:
- flagged_by_toxicity: Плагин AI определил этот контент как токсичный и отправил жалобу.
- flagged_by_nsfw: Плагин AI определил как минимум одно из прикрепленных изображений как NSFW и отправил жалобу.
- reports:
- overall_sentiment:
- title: "Общее настроение"
- description: 'На диаграмме сравнивается количество публикаций, классифицированных как позитивные или негативные на основе того, превышают ли их оценки установленные пороговые значения. Нейтральные публикации и личные сообщения (ЛС) в расчет не включаются. Классификация выполнена с помощью модели "cardiffnlp/twitter-roberta-base-sentiment-latest"'
- xaxis: "Позитивные(%)"
- yaxis: "Дата"
- emotion_admiration:
- title: "\U0001F929 Восхищение"
- description: "Публикации, классифицированные AI по эмоции «восхищение» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_amusement:
- title: "\U0001F604 Веселье"
- description: "Публикации, классифицированные AI по эмоции «веселье» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_anger:
- title: "\U0001F620 Гнев"
- description: "Публикации, классифицированные AI по эмоции «гнев» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_annoyance:
- title: "\U0001F612 Недовольство"
- description: "Публикации, классифицированные AI по эмоции «недовольство» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_approval:
- title: "\U0001F44D Одобрение"
- description: "Публикации, классифицированные AI по эмоции «одобрение» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_caring:
- title: "\U0001F917 Забота"
- description: "Публикации, классифицированные AI по эмоции «забота» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_confusion:
- title: "\U0001F615 Замешательство"
- description: "Публикации, классифицированные AI по эмоции «замешательство» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_curiosity:
- title: "\U0001F914 Любопытство"
- description: "Публикации, классифицированные AI по эмоции «любопытство» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_desire:
- title: "\U0001F60D Восторг"
- description: "Публикации, классифицированные AI по эмоции «восторг» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_disappointment:
- title: "\U0001F61E Разочарование"
- description: "Публикации, классифицированные AI по эмоции «разочарование» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_disapproval:
- title: "\U0001F44E Неодобрение"
- description: "Публикации, классифицированные AI по эмоции «неодобрение» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_disgust:
- title: "\U0001F922 Отвращение"
- description: "Публикации, классифицированные AI по эмоции «отвращение» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_embarrassment:
- title: "\U0001F633 Смущение"
- description: "Публикации, классифицированные AI по эмоции «смущение» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_excitement:
- title: "\U0001F92A Волнение"
- description: "Публикации, классифицированные AI по эмоции «волнение» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_fear:
- title: "\U0001F628 Страх"
- description: "Публикации, классифицированные AI по эмоции «страх» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_gratitude:
- title: "\U0001F64F Благодарность"
- description: "Публикации, классифицированные AI по эмоции «благодарность» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_grief:
- title: "\U0001F622 Печаль"
- description: "Публикации, классифицированные AI по эмоции «печаль» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_joy:
- title: "\U0001F60A Радость"
- description: "Публикации, классифицированные AI по эмоции «радость» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_love:
- title: '❤️ Любовь'
- description: "Публикации, классифицированные AI по эмоции «любовь» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_nervousness:
- title: "\U0001F630 Тревога"
- description: "Публикации, классифицированные AI по эмоции «тревога» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_neutral:
- title: "\U0001F610 Нейтральность"
- description: "Публикации, классифицированные AI по эмоции «нейтральность» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_optimism:
- title: "\U0001F31F Оптимизм"
- description: "Публикации, классифицированные AI по эмоции «оптимизм» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_pride:
- title: "\U0001F981 Гордость"
- description: "Публикации, классифицированные AI по эмоции «гордость» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_realization:
- title: "\U0001F4A1 Озарение"
- description: "Публикации, классифицированные AI по эмоции «озарение» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_relief:
- title: "\U0001F60C Облегчение"
- description: "Публикации, классифицированные AI по эмоции «облегчение» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_remorse:
- title: "\U0001F614 Сожаление"
- description: "Публикации, классифицированные AI по эмоции «сожаление» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_sadness:
- title: "\U0001F62D Грусть"
- description: "Публикации, классифицированные AI по эмоции «грусть» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- emotion_surprise:
- title: "\U0001F632 Удивление"
- description: "Публикации, классифицированные AI по эмоции «удивление» с помощью модели 'SamLowe/roberta-base-go_emotions'."
- discourse_ai:
- ai_artifact:
- view_source: "Посмотреть источник"
- view_changes: "Просмотреть изменения"
- unknown_model: "Неизвестная модель AI"
- tools:
- custom_name: "%{name} (пользовательский)"
- presets:
- browse_web_jina:
- name: "Просмотр веб-страниц (jina.ai)"
- exchange_rate:
- name: "Обменный курс"
- stock_quote:
- name: "Котировка акций (AlphaVantage)"
- image_generation:
- name: "Генератор изображений Flux (Together.ai)"
- empty_tool:
- name: "Начать с нуля..."
- ai_helper:
- errors:
- completion_request_failed: "При попытке дать рекомендации произошла ошибка. Попробуйте еще раз."
- prompts:
- translate: Перевести на %{language}
- generate_titles: Предложить названия для темы
- proofread: Вычитать текст
- markdown_table: Сгенерировать таблицу в формате Markdown
- custom_prompt: "Пользовательский запрос"
- explain: "Объяснить"
- illustrate_post: "Проиллюстрировать публикацию"
- replace_dates: "Умные даты"
- painter:
- attribution:
- stable_diffusion_xl: "Изображение от Stable Diffusion XL"
- dall_e_3: "Изображение от DALL-E 3"
- image_caption:
- attribution: "Подпись к изображению от AI"
- share_ai:
- read_more: "Прочитать полное содержимое"
- onebox_title: "Разговор AI с пользователем %{llm_name}"
- formatted_excerpt: "Разговор AI с пользователем %{llm_name}:\n %{excerpt}"
- title: "%{title} — разговор AI — %{site_name}"
- errors:
- not_allowed: "Вы не можете поделиться этой темой"
- other_people_in_pm: "Личные сообщения с другими людьми нельзя делать общедоступными"
- other_content_in_pm: "Личные сообщения, содержащие публикации других людей, нельзя делать общедоступными"
- failed_to_share: "Не удалось поделиться разговором"
- conversation_deleted: "Общий доступ к разговору успешно удален"
- spam_detection:
- flag_reason: "Помечено как спам AI для Discourse"
- silence_reason: "Пользователь автоматически заморожен AI для Discourse"
- invalid_error_type: "Указан неверный тип ошибки"
- unexpected: "Возникла непредвиденная ошибка"
- bot_user_update_failed: "Не удалось обновить пользователя-бота для проверки спама"
- ai_bot:
- reply_error: "Извините, похоже, наша система столкнулась с неожиданной проблемой при попытке ответить.\n\n[details='Сведения об ошибке']\n%{details}\n[/details]"
- default_pm_prefix: "[Личное сообщение от AI-бота без названия]"
- personas:
- default_llm_required: "Для включения чата требуется модель LLM по умолчанию"
- cannot_delete_system_persona: "Системные персоны нельзя удалить, вместо этого отключите их"
- cannot_edit_system_persona: "Системные персоны можно только переименовать, вы не можете редактировать инструменты или системный запрос, вместо этого отключите их и создайте копию"
- github_helper:
- name: "Помощник по GitHub"
- description: "AI-бот, специализирующийся на оказании помощи в решении задач и вопросов, связанных с GitHub"
- general:
- name: Помощник по форуму
- description: "AI-бот общего назначения, способный выполнять различные задачи."
- artist:
- name: Художник
- description: "AI-бот, специализирующийся на генерации изображений"
- sql_helper:
- name: Помощник по SQL
- description: "AI-бот, специализирующийся на создании SQL-запросов в этом экземпляре Discourse"
- settings_explorer:
- name: Обозреватель настроек
- description: "AI-бот, специализирующийся на помощи в изучении настроек сайта Discourse"
- creative:
- name: Автор
- description: "AI-бот без внешних интеграций, специализирующийся на творческих задачах"
- dall_e3:
- name: "DALL-E 3"
- description: "AI-бот, специализирующийся на генерации изображений с использованием DALL-E 3"
- discourse_helper:
- name: "Помощник по Discourse"
- description: "AI-бот, специализирующийся на решении задач, связанных с Discourse"
- web_artifact_creator:
- name: "Создатель веб-артефактов"
- description: "AI-бот, специализирующийся на создании интерактивных веб-артефактов"
- custom_prompt:
- name: "Пользовательский запрос"
- smart_dates:
- name: "Умные даты"
- topic_not_found: "Сводка недоступна: тема не найдена!"
- summarizing: "Аналитик темы"
- searching: "Поиск: '%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "Максимальное количество результатов"
- google:
- base_query:
- name: "Базовый поисковый запрос"
- description: "Базовый запрос для использования при поиске. Примеры: 'site:example.com' будет включать только результаты с example.com, 'before:2022-01-01' будет включать только результаты с 2021 года и ранее. Этот текст добавляется к поисковому запросу."
- read:
- read_private:
- name: "Читать приватные темы"
- description: "Разрешить доступ ко всем темам, к которым есть доступ у пользователя (по умолчанию включены только публичные темы)"
- search:
- search_private:
- name: "Поиск по приватным темам"
- description: "Включить в результаты поиска все темы, к которым есть доступ у пользователя (по умолчанию включены только публичные темы)"
- max_results:
- name: "Максимальное количество результатов"
- description: "Максимальное количество результатов, включаемых в поиск, — если будут использоваться пустые правила по умолчанию, а количество будет масштабироваться в зависимости от используемой модели. Максимальное значение — 100."
- base_query:
- name: "Базовый поисковый запрос"
- description: "Базовый запрос, используемый при поиске. Пример: '#urgent' добавит '#urgent' к поисковому запросу и будет включать только темы со срочной категорией или тегом."
- tool_summary:
- update_artifact: "Обновить веб-артефакт"
- create_artifact: "Создать веб-артефакт"
- web_browser: "Просмотр веб-страниц"
- github_search_files: "Поиск файлов на GitHub"
- github_search_code: "Поиск кода на GitHub"
- github_file_content: "Контент файла на GitHub"
- github_pull_request_diff: "Сравнение изменений в запросе на вытягивание на GitHub"
- random_picker: "Случайный выбор"
- categories: "Вывод списка категорий"
- search: "Искать"
- tags: "Вывод списка тегов"
- time: "Время"
- summarize: "Сводка"
- image: "Сгенерировать изображение"
- google: "Искать в Google"
- read: "Прочитать тему"
- setting_context: "Поиск контекста настроек сайта"
- schema: "Найти схему базы данных"
- search_settings: "Поиск настроек сайта"
- dall_e: "Сгенерировать изображение"
- search_meta_discourse: "Поиск на Meta Discourse"
- javascript_evaluator: "Оценить JavaScript"
- tool_help:
- update_artifact: "Обновить веб-артефакт с помощью AI-бота"
- create_artifact: "Создать веб-артефакт с помощью AI-бота"
- web_browser: "Просмотр веб-страницы с помощью AI-бота"
- github_search_code: "Поиск кода в репозитории GitHub"
- github_search_files: "Поиск файлов в репозитории GitHub"
- github_file_content: "Извлечение контента файлов из репозитория на GitHub"
- github_pull_request_diff: "Получить сравнение изменений в запросе на вытягивание на GitHub"
- random_picker: "Выбирает случайное число или случайный элемент списка"
- categories: "Вывод всех общедоступных категорий на форуме"
- search: "Поиск по всем общедоступным темам на форуме"
- tags: "Вывод всех тегов на форуме"
- time: "Определить время в разных часовых поясах"
- summary: "Сводка по теме"
- image: "Генерировать изображение с помощью Stable Diffusion"
- google: "Поиск в Google по запросу"
- read: "Читать общедоступную тему на форуме"
- setting_context: "Поиск контекста настроек сайта"
- schema: "Найти схему базы данных"
- search_settings: "Поиск по настройкам сайта"
- dall_e: "Создать изображение с помощью DALL-E 3"
- search_meta_discourse: "Поиск на Meta Discourse"
- javascript_evaluator: "Оценить JavaScript"
- tool_description:
- update_artifact: "Обновлен веб-артефакт с помощью AI-бота"
- web_browser: "Чтение: %{url}"
- github_search_files: "Выполнен поиск «%{keywords}» в %{repo}/%{branch}"
- github_search_code: "Выполнен поиск «%{query}» в %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "Получен контент файла %{file_paths} из репозитория %{repo_name}@%{branch}"
- random_picker: "Выбор из %{options}, выбрано: %{result}"
- read: "Чтение: %{title}"
- time: "Время по часовому поясу %{timezone} — %{time}"
- summarize: "Получена сводка: %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "Найдена %{count} категория"
- few: "Найдены %{count} категории"
- many: "Найдено %{count} категорий"
- other: "Найдено %{count} категории"
- tags:
- one: "Найден %{count} тег"
- few: "Найдено %{count} тега"
- many: "Найдено %{count} тегов"
- other: "Найдены %{count} тега"
- search:
- one: "Найден %{count} результат по запросу «%{query}»"
- few: "Найдены %{count} результата по запросу «%{query}»"
- many: "Найдено %{count} результатов по запросу «%{query}»"
- other: "Найдено %{count} результата по запросу «%{query}»"
- search_meta_discourse:
- one: "Найден %{count} результат по запросу «%{query}»"
- few: "Найдены %{count} результата по запросу «%{query}»"
- many: "Найдено %{count} результатов по запросу «%{query}»"
- other: "Найдено %{count} результата по запросу «%{query}»"
- google:
- one: "Найден %{count} результат по запросу «%{query}»"
- few: "Найдены %{count} результата по запросу «%{query}»"
- many: "Найдено %{count} результатов по запросу «%{query}»"
- other: "Найдено %{count} результата по запросу «%{query}»"
- setting_context: "Чтение контекста для: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "Найден %{count} результат по запросу «%{query}»"
- few: "Найдено %{count} результата по запросу «%{query}»"
- many: "Найдено %{count} результатов по запросу «%{query}»"
- other: "Найдено %{count} результата по запросу «%{query}»"
- summarization:
- configuration_hint:
- one: "Сначала настройте следующие параметры: %{settings}"
- few: "Сначала настройте следующие параметры: %{settings}"
- many: "Сначала настройте следующие параметры: %{settings}"
- other: "Сначала настройте следующие параметры: %{settings}"
- chat:
- no_targets: "За выбранный период сообщений не было."
- sentiment:
- reports:
- overall_sentiment: "Общее настроение (позитивное — негативное)"
- post_emotion:
- sadness: "Грусть \U0001F622"
- surprise: "Удивление \U0001F631"
- neutral: "Нейтральность \U0001F610"
- fear: "Страх \U0001F628"
- anger: "Гнев \U0001F621"
- joy: "Радость \U0001F600"
- disgust: "Отвращение \U0001F922"
- sentiment_analysis:
- positive: "Позитивное"
- negative: "Негативное"
- neutral: "Нейтральность"
- llm:
- configuration:
- disable_module_first: "Сначала вам нужно отключить %{setting}."
- set_llm_first: "Сначала задайте %{setting}"
- model_unreachable: "Мы не смогли получить ответ от этой модели. Проверьте настройки."
- invalid_seeded_model: "Эту модель нельзя использовать с этой функцией"
- must_select_model: "Сначала вам нужно выбрать LLM"
- endpoints:
- not_configured: "%{display_name} (не настроено)"
- configuration_hint:
- one: "Убедитесь, что настроен параметр `%{settings}`."
- few: "Убедитесь, что настроены параметры: `%{settings}`."
- many: "Убедитесь, что настроены параметры: `%{settings}`."
- other: "Убедитесь, что настроены параметры: `%{settings}`."
- delete_failed:
- one: "Мы не смогли удалить эту модель, потому что ее использует %{settings}. Обновите настройки и попробуйте еще раз."
- few: "Мы не смогли удалить эту модель, потому что ее используют %{settings}. Обновите настройки и попробуйте еще раз."
- many: "Мы не смогли удалить эту модель, потому что ее используют %{settings}. Обновите настройки и попробуйте еще раз."
- other: "Мы не смогли удалить эту модель, потому что ее используют %{settings}. Обновите настройки и попробуйте еще раз."
- cannot_edit_builtin: "Встроенную модель редактировать нельзя."
- embeddings:
- delete_failed: "Эта модель сейчас используется. Сначала обновите параметр `ai embeddings selected model`."
- cannot_edit_builtin: "Встроенную модель редактировать нельзя."
- configuration:
- disable_embeddings: "Сначала вам необходимо отключить настройку 'ai embeddings enabled'."
- choose_model: "Сначала задайте 'ai embeddings selected model'."
- llm_models:
- missing_provider_param: "Параметр %{param} не может быть пустым"
- bedrock_invalid_url: "Чтобы использовать эту модель, заполните все поля."
- ai_staff_action_logger:
- updated: "обновлена"
- removed: "отозваны"
- errors:
- quota_exceeded: "Вы превысили квоту для этой модели. Повторите попытку через %{relative_time}."
- quota_required: "Необходимо указать максимальное количество токенов или использований для этой модели"
- no_query_specified: Параметр запроса обязателен, укажите его.
- no_user_for_persona: Указанная персона не имеет связанного с ней пользователя.
- persona_not_found: Указанная персона не существует. Проверьте параметры persona_name или persona_id.
- no_user_specified: Параметры user_unique_id или username обязательны, укажите нужное.
- user_not_found: Указанный пользователь не существует. Проверьте параметр username.
- persona_disabled: Указанная персона отключена. Проверьте параметры persona_name или persona_id.
- no_default_llm: У персоны должен быть определен параметр default_llm.
- user_not_allowed: Пользователю не разрешено участвовать в теме.
- prompt_message_length: Сообщение %{idx} превышает ограничение в 1000 символов.
diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml
deleted file mode 100644
index cf975e35..00000000
--- a/config/locales/server.sk.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sk:
- reports:
- overall_sentiment:
- yaxis: "Dátum"
- emotion_neutral:
- title: "\U0001F610 Neutrálna"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Hľadať"
- tags: "Zoznam značiek"
- time: "Čas"
- summarize: "Zhrnúť"
- summarization:
- chat:
- no_targets: "Počas vybraného obdobia neboli zaznamenané žiadne správy."
- sentiment:
- reports:
- post_emotion:
- neutral: "Neutrálna \U0001F610"
- sentiment_analysis:
- neutral: "Neutrálna"
- ai_staff_action_logger:
- updated: "aktualizované"
- removed: "odstránené"
diff --git a/config/locales/server.sl.yml b/config/locales/server.sl.yml
deleted file mode 100644
index cda98ac1..00000000
--- a/config/locales/server.sl.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sl:
- reports:
- overall_sentiment:
- yaxis: "Datum"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Išči"
- time: "Čas"
diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml
deleted file mode 100644
index 09474d2c..00000000
--- a/config/locales/server.sq.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sq:
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Kërko"
- time: "Koha"
- ai_staff_action_logger:
- removed: "u hoq"
diff --git a/config/locales/server.sr.yml b/config/locales/server.sr.yml
deleted file mode 100644
index b62eb641..00000000
--- a/config/locales/server.sr.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sr:
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Pretraži"
- time: "Vreme"
diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml
deleted file mode 100644
index 2f215d4c..00000000
--- a/config/locales/server.sv.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sv:
- reports:
- overall_sentiment:
- yaxis: "Datum"
- emotion_neutral:
- title: "\U0001F610 Neutralt"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Sök"
- time: "Tid"
- summarize: "Sammanfatta"
- summarization:
- chat:
- no_targets: "Det fanns inga meddelanden under den valda perioden."
- sentiment:
- reports:
- post_emotion:
- neutral: "Neutralt \U0001F610"
- sentiment_analysis:
- neutral: "Neutralt"
- ai_staff_action_logger:
- updated: "uppdaterade"
- removed: "borttagen"
diff --git a/config/locales/server.sw.yml b/config/locales/server.sw.yml
deleted file mode 100644
index 87f11162..00000000
--- a/config/locales/server.sw.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-sw:
- reports:
- overall_sentiment:
- yaxis: "Tarehe"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Tafuta"
- time: "Muda"
diff --git a/config/locales/server.te.yml b/config/locales/server.te.yml
deleted file mode 100644
index 3d4a0816..00000000
--- a/config/locales/server.te.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-te:
- reports:
- overall_sentiment:
- yaxis: "తేదీ"
- emotion_neutral:
- title: "\U0001F610 తటస్థ"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "శోధించండి"
- time: "కాలం"
- sentiment:
- reports:
- post_emotion:
- neutral: "తటస్థ \U0001F610"
- sentiment_analysis:
- neutral: "తటస్థ"
- ai_staff_action_logger:
- updated: "నవీకరించబడింది"
- removed: "తీసివేయబడింది"
diff --git a/config/locales/server.th.yml b/config/locales/server.th.yml
deleted file mode 100644
index 60bd8d3a..00000000
--- a/config/locales/server.th.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-th:
- reports:
- overall_sentiment:
- yaxis: "วันที่"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "ค้นหา"
- time: "เวลา"
- ai_staff_action_logger:
- updated: "อัปเดตแล้ว"
diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml
deleted file mode 100644
index 8639baa0..00000000
--- a/config/locales/server.tr_TR.yml
+++ /dev/null
@@ -1,446 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-tr_TR:
- discourse_automation:
- ai:
- flag_types:
- review: "Gönderiyi inceleme kuyruğuna ekleyin"
- spam: "Spam olarak işaretleyin ve gönderiyi gizleyin"
- spam_silence: "Spam olarak işaretleyin, gönderiyi gizleyin ve kullanıcıyı susturun"
- scriptables:
- llm_triage:
- title: AI kullanarak gönderileri triyajla
- description: "Büyük bir dil modeli kullanarak gönderileri triyajla"
- flagged_post: |
-
Modelden gelen yanıt:
-
%%LLM_RESPONSE%%
- %%AUTOMATION_NAME%% kuralı ile tetiklenir.
- llm_report:
- title: AI kullanarak periyodik raporla
- description: "Geniş bir dil modeline dayalı periyodik raporla"
- site_settings:
- discourse_ai_enabled: "Discourse YZ eklentisini etkinleştirin."
- ai_toxicity_enabled: "Toksiklik modülünü etkinleştirin."
- ai_toxicity_inference_service_api_endpoint: "API'nin toksiklik modülü için çalıştığı URL"
- ai_toxicity_inference_service_api_key: "Toksiklik API'si için API anahtarı"
- ai_toxicity_inference_service_api_model: "Çıkarım için kullanılacak model. Çok dilli model İtalyanca, Fransızca, Rusça, Portekizce, İspanyolca ve Türkçe ile çalışır."
- ai_toxicity_flag_automatically: "Yapılandırılan eşiklerin üstündeki gönderilere / sohbet mesajlarına otomatik olarak bayrak ekleyin."
- ai_toxicity_flag_threshold_toxicity: "Toksiklik: Bir tartışmayı bırakmanıza veya bakış açınızı paylaşmaktan vazgeçmenize neden olabilecek kaba, saygısız veya mantıksız bir yorum"
- ai_toxicity_flag_threshold_severe_toxicity: "Şiddetli Toksiklik: Bir tartışmayı bırakmanıza veya bakış açınızı paylaşmaktan vazgeçmenize neden olabilecek çok nefret dolu, saldırgan veya saygısız bir yorum"
- ai_toxicity_flag_threshold_obscene: "Müstehcen"
- ai_toxicity_flag_threshold_identity_attack: "Kimlik Saldırısı"
- ai_toxicity_flag_threshold_insult: "Hakaret"
- ai_toxicity_flag_threshold_threat: "Tehdit"
- ai_toxicity_flag_threshold_sexual_explicit: "Cinsel İçerikli"
- ai_toxicity_groups_bypass: "Bu gruplardaki kullanıcıların gönderileri toksiklik modülü tarafından sınıflandırılmaz."
- ai_sentiment_enabled: "Duyarlılık modülünü etkinleştirin."
- ai_sentiment_inference_service_api_endpoint: "Duyarlılık modülü için API'nin çalıştığı URL"
- ai_sentiment_inference_service_api_key: "Duyarlılık API'si için API anahtarı"
- ai_sentiment_models: "Çıkarım için kullanılacak modeller. Duygu, gönderiyi pozitif/nötr/negatif alanda sınıflandırır. Duygu, öfke/iğrenme/korku/sevinç/nötr/üzüntü/şaşırma alanına göre sınıflandırır."
- ai_nsfw_detection_enabled: "NSFW modülünü etkinleştirin."
- ai_nsfw_inference_service_api_endpoint: "NSFW modülü için API'nin çalıştığı URL"
- ai_nsfw_inference_service_api_key: "NSFW API'si için API anahtarı"
- ai_nsfw_flag_automatically: "Yapılandırılan eşiklerin üstünde olan NSFW gönderileri otomatik olarak işaretleyin."
- ai_nsfw_flag_threshold_general: "Bir görüntünün NSFW olarak değerlendirilmesi için Genel Eşik."
- ai_nsfw_flag_threshold_drawings: "Bir çizimin NSFW olarak değerlendirilmesi için eşik."
- ai_nsfw_flag_threshold_hentai: "Hentai olarak sınıflandırılan bir görüntünün NSFW olarak değerlendirilmesi için eşik."
- ai_nsfw_flag_threshold_porn: "Porno olarak sınıflandırılan bir görüntünün NSFW olarak değerlendirilmesi için eşik."
- ai_nsfw_flag_threshold_sexy: "Seksi olarak sınıflandırılan bir görüntünün NSFW olarak değerlendirilmesi için eşik."
- ai_nsfw_models: "NSFW çıkarımı için kullanılacak modeller."
- ai_helper_enabled: "YZ yardımcısını etkinleştirin."
- composer_ai_helper_allowed_groups: "Bu gruplardaki kullanıcılar, bestecide YZ yardımcı düğmesini görür."
- ai_helper_allowed_in_pm: "Kişisel mesajlarda bestecinin YZ yardımcıyı etkinleştirin."
- ai_helper_model: "YZ yardımcı için kullanılacak model."
- ai_helper_custom_prompts_allowed_groups: "Bu gruplardaki kullanıcılar AI yardımcısında özel istem seçeneğini görürler."
- ai_helper_automatic_chat_thread_title_delay: "AI yardımcısının sohbet dizisi başlığını otomatik olarak ayarlamasından önceki dakika cinsinden gecikme."
- ai_helper_automatic_chat_thread_title: "Sohbet konu başlıklarını konu içeriklerine göre otomatik olarak ayarlayın."
- ai_helper_illustrate_post_model: "Besteci AI yardımcısının gönderiyi çizme özelliği için kullanılacak model"
- ai_helper_enabled_features: "YZ yardımcısında etkinleştirilecek özellikleri seçin."
- post_ai_helper_allowed_groups: "Gönderilerde YZ Yardımcısı özelliklerine erişmesine izin verilen kullanıcı grupları"
- ai_helper_image_caption_model: "Görüntü alt yazıları oluşturmak için kullanılacak modeli seçin"
- ai_auto_image_caption_allowed_groups: "Bu gruplardaki kullanıcılar otomatik görüntü alt yazısı eklemeyi değiştirebilir."
- ai_embeddings_selected_model: "Gömme oluşturmak için seçili modeli kullanın."
- ai_embeddings_generate_for_pms: "Kişisel mesajlar için gömmeler oluşturun."
- ai_embeddings_semantic_related_topics_enabled: "İlgili konular için Semantik Arama'yı kullanın."
- ai_embeddings_semantic_related_topics: "İlgili konu bölümünde gösterilecek maksimum konu sayısı."
- ai_embeddings_backfill_batch_size: "Her 15 dakikada bir geri doldurulacak gömme sayısı."
- ai_embeddings_semantic_search_enabled: "Tam sayfa semantik aramayı etkinleştirin."
- ai_embeddings_semantic_quick_search_enabled: "Arama menüsü açılır penceresinde semantik arama seçeneğini etkinleştirin."
- ai_embeddings_semantic_related_include_closed_topics: "Kapalı konuları semantik arama sonuçlarına dâhil edin"
- ai_embeddings_semantic_search_hyde_model: "Anlamsal arama sırasında daha iyi sonuçlar elde etmek için anahtar kelimeleri genişletmek üzere kullanılan model"
- ai_embeddings_per_post_enabled: Her gönderi için yerleştirmeler oluşturun
- ai_summarization_model: "Özetleme için kullanılacak model"
- ai_custom_summarization_allowed_groups: "Grupların yeni özetler oluşturmasına izin verilir."
- ai_pm_summarization_allowed_groups: "Kişisel mesajlarda özet oluşturma ve görüntüleme yetkisi olan gruplar."
- ai_summary_gists_enabled: "Konulardaki son yanıtların kısa özetlerini otomatik olarak oluşturun"
- ai_summary_gists_allowed_groups: "Popüler konular listesindeki özetleri görme yetkisine sahip gruplar."
- ai_summary_backfill_maximum_topics_per_hour: "Saat başına doldurulacak konu özeti sayısı."
- ai_bot_enabled: "YZ Botu modülünü etkinleştirin."
- ai_bot_enable_chat_warning: "Kişisel mesaj sohbeti başlatıldığında bir uyarı görüntüleyin. Çeviri dizesi düzenlenerek geçersiz kılınabilir: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "GPT Botunun Kişisel Mesaja erişimi olduğunda, bu grupların üyelerine yanıt verir."
- ai_bot_debugging_allowed_groups: "Bu grupların gönderilerde ham YZ isteğini ve yanıtını görüntüleyen bir hata ayıklama düğmesi görmesine izin verin"
- ai_bot_public_sharing_allowed_groups: "Bu grupların, herkese açık benzersiz bir bağlantı aracılığıyla YZ kişisel mesajlarını herkese açık şekilde paylaşmasına izin verin. Not: Siteniz giriş yapılmasını gerektiriyorsa paylaşımlar da giriş yapılmasını gerektirir."
- ai_bot_add_to_header: "Bir YZ Botu ile bir kişisel mesaj başlatmak için başlıkta bir düğme gösterin"
- ai_bot_github_access_token: "GitHub AI araçlarıyla kullanım için GitHub erişim token'ı (arama desteği için gereklidir)"
- ai_stability_api_key: "stability.ai API'si için API anahtarı"
- ai_stability_engine: "stability.ai API'si için kullanılacak görüntü oluşturma motoru"
- ai_stability_api_url: "stability.ai API'si için URL"
- ai_google_custom_search_api_key: "Google Özel Arama API'sı için API anahtarı bkz.: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "Google Özel Arama API'sı için CX"
- reviewables:
- reasons:
- flagged_by_toxicity: YZ eklentisi, toksik olarak sınıflandırdıktan sonra buna bayrak ekledi.
- flagged_by_nsfw: YZ eklentisi, eklenen resimlerden en az birini NSFW olarak sınıflandırdıktan sonra buna bayrak ekledi.
- reports:
- overall_sentiment:
- title: "Genel duyarlılık"
- description: 'Grafik, olumlu veya olumsuz olarak sınıflandırılan gönderilerin sayısını karşılaştırır. Bunlar, pozitif veya negatif puanlar belirlenen eşik puanından fazla olduğunda hesaplanır. Bu, nötr gönderilerin gösterilmediği anlamına gelir. Kişisel mesajlar da hariç tutulmuştur. "cardiffnlp/twitter-roberta-base-sentiment-latest" ile sınıflandırılır'
- xaxis: "Pozitif(%)"
- yaxis: "Tarih"
- emotion_admiration:
- title: "\U0001F929 Hayranlık"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla hayranlık duygusu ile sınıflandırılan gönderiler."
- emotion_amusement:
- title: "\U0001F604 Eğlenme"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla eğlenme duygusu ile sınıflandırılan gönderiler."
- emotion_anger:
- title: "\U0001F620 Öfke"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla öfke duygusu ile sınıflandırılan gönderiler."
- emotion_annoyance:
- title: "\U0001F612 Rahatsız olma"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla rahatsız olma duygusu ile sınıflandırılan gönderiler."
- emotion_approval:
- title: "\U0001F44D Onaylama"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla onaylama duygusu ile sınıflandırılan gönderiler."
- emotion_caring:
- title: "\U0001F917 Önemseme"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla önemseme duygusu ile sınıflandırılan gönderiler."
- emotion_confusion:
- title: "\U0001F615 Kafa karışıklığı"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla kafa karışıklığı duygusu ile sınıflandırılan gönderiler."
- emotion_curiosity:
- title: "\U0001F914 Merak"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla merak duygusu ile sınıflandırılan gönderiler."
- emotion_desire:
- title: "\U0001F60D Arzu"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla arzu duygusu ile sınıflandırılan gönderiler."
- emotion_disappointment:
- title: "\U0001F61E Hayal kırıklığı"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla hayal kırıklığı duygusu ile sınıflandırılan gönderiler."
- emotion_disapproval:
- title: "\U0001F44E Onaylamama"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla onaylamama duygusu ile sınıflandırılan gönderiler."
- emotion_disgust:
- title: "\U0001F922 İğrenme"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla iğrenme duygusu ile sınıflandırılan gönderiler."
- emotion_embarrassment:
- title: "\U0001F633 Utanma"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla utanma duygusu ile sınıflandırılan gönderiler."
- emotion_excitement:
- title: "\U0001F92A Heyecan"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla heyecan duygusu ile sınıflandırılan gönderiler."
- emotion_fear:
- title: "\U0001F628 Korku"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla korku duygusu ile sınıflandırılan gönderiler."
- emotion_gratitude:
- title: "\U0001F64F Minnettarlık"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla minnettarlık duygusu ile sınıflandırılan gönderiler."
- emotion_grief:
- title: "\U0001F622 Keder"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla keder duygusu ile sınıflandırılan gönderiler."
- emotion_joy:
- title: "\U0001F60A Neşe"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla neşe duygusu ile sınıflandırılan gönderiler."
- emotion_love:
- title: '❤️ Sevgi'
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla sevgi duygusu ile sınıflandırılan gönderiler."
- emotion_nervousness:
- title: "\U0001F630 Gerginlik"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla gerginlik duygusu ile sınıflandırılan gönderiler."
- emotion_neutral:
- title: "\U0001F610 Nötr"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla nötr duygusu ile sınıflandırılan gönderiler."
- emotion_optimism:
- title: "\U0001F31F İyimserlik"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla iyimserlik duygusu ile sınıflandırılan gönderiler."
- emotion_pride:
- title: "\U0001F981 Gurur"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla gurur duygusu ile sınıflandırılan gönderiler."
- emotion_realization:
- title: "\U0001F4A1 Farkına varma"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla farkına varma duygusu ile sınıflandırılan gönderiler."
- emotion_relief:
- title: "\U0001F60C Rahatlama"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla rahatlama duygusu ile sınıflandırılan gönderiler."
- emotion_remorse:
- title: "\U0001F614 Pişmanlık"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla pişmanlık duygusu ile sınıflandırılan gönderiler."
- emotion_sadness:
- title: "\U0001F62D Üzüntü"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla üzüntü duygusu ile sınıflandırılan gönderiler."
- emotion_surprise:
- title: "\U0001F632 Şaşırma"
- description: "\"SamLowe/roberta-base-go_emotions\" modeli kullanılarak YZ aracılığıyla şaşırma duygusu ile sınıflandırılan gönderiler."
- discourse_ai:
- ai_artifact:
- view_source: "Kaynağı görüntüle"
- view_changes: "Değişiklikleri görüntüle"
- unknown_model: "Bilinmeyen YZ modeli"
- tools:
- custom_name: "%{name} (özel)"
- presets:
- browse_web_jina:
- name: "Web'de gezinin (jina.ai)"
- exchange_rate:
- name: "Döviz kuru"
- stock_quote:
- name: "Hisse senedi fiyat teklifi (AlphaVantage)"
- image_generation:
- name: "Flux görüntü oluşturucu (Together.ai)"
- empty_tool:
- name: "Boştan başla..."
- ai_helper:
- errors:
- completion_request_failed: "Öneriler sunulmaya çalışılırken bir şeyler ters gitti. Lütfen tekrar deneyin."
- prompts:
- translate: '%{language} diline çevir'
- generate_titles: Konu başlıkları öner
- proofread: Yazım hataları düzeltilmiş metin
- markdown_table: Markdown tablosu oluştur
- custom_prompt: "Özel İstem"
- explain: "Açıkla"
- illustrate_post: "Gönderiyi göster"
- replace_dates: "Akıllı tarihler"
- painter:
- attribution:
- stable_diffusion_xl: "Stable Diffusion XL'den görüntü"
- dall_e_3: "DALL-E 3'den görüntü"
- image_caption:
- attribution: "AI tarafından altyazılı"
- share_ai:
- read_more: "Tam metni okuyun"
- onebox_title: "%{llm_name} ile YZ Konuşması"
- formatted_excerpt: "%{llm_name} ile YZ Konuşması:\n %{excerpt}"
- title: "%{title} - YZ Konuşması - %{site_name}"
- errors:
- not_allowed: "Bu konuyu paylaşmanıza izin verilmiyor"
- other_people_in_pm: "Diğer insanlarla olan kişisel mesajlar herkese açık olarak paylaşılamaz"
- other_content_in_pm: "Başkalarına ait gönderileri içeren kişisel mesajlar herkese açık olarak paylaşılamaz"
- failed_to_share: "Konuşma paylaşılamadı"
- conversation_deleted: "Konuşma paylaşımı başarıyla silindi"
- spam_detection:
- flag_reason: "Discourse AI tarafından istenmeyen içerik olarak bayrak eklendi"
- silence_reason: "Kullanıcı Discourse AI tarafından otomatik olarak susturuldu"
- invalid_error_type: "Geçersiz hata türü sağlandı"
- unexpected: "Beklenmeyen bir hata oluştu"
- bot_user_update_failed: "İstenmeyen içerik tarama botu kullanıcısı güncellenemedi"
- ai_bot:
- reply_error: "Üzgünüz, sistemimiz yanıtlamaya çalışırken beklenmeyen bir sorunla karşılaşmış gibi görünüyor.\n\n[details='Hata ayrıntıları']\n%{details}\n[/details]"
- default_pm_prefix: "[Adsız YZ botu kişisel mesajı]"
- personas:
- default_llm_required: "Sohbeti etkinleştirmeden önce varsayılan LLM modeli gereklidir"
- cannot_delete_system_persona: "Sistem kişilikleri silinemez, lütfen bunun yerine devre dışı bırakın"
- cannot_edit_system_persona: "Sistem kişileri yalnızca yeniden adlandırılabilir, araçları veya sistem istemini düzenleyemezsiniz, bunun yerine devre dışı bırakıp bir kopyasını oluşturabilirsiniz"
- github_helper:
- name: "GitHub Yardımcısı"
- description: "GitHub ile ilgili görevlere ve sorulara yardımcı olma konusunda uzmanlaşmış YZ Botu"
- general:
- name: Forum Yardımcısı
- description: "Çeşitli görevleri yerine getirebilen genel amaçlı AI Botu"
- artist:
- name: Sanatçı
- description: "Görsel üretme konusunda uzmanlaşmış AI Bot"
- sql_helper:
- name: SQL Yardımcısı
- description: "Bu Discourse örneğinde SQL sorguları oluşturmaya yardımcı olma konusunda uzmanlaşmış AI Bot"
- settings_explorer:
- name: Ayarlar Gezgini
- description: "Discourse site ayarlarını keşfetmeye yardımcı olma konusunda uzmanlaşmış AI Bot"
- creative:
- name: Yaratıcı
- description: "Yaratıcı görevlerde uzmanlaşmış, harici entegrasyonları olmayan AI Bot"
- dall_e3:
- name: "DALL-E 3"
- description: "DALL-E 3 kullanarak görüntü üretme konusunda uzmanlaşmış AI Bot"
- discourse_helper:
- name: "Discourse Yardımcısı"
- description: "Discourse ile ilgili görevlere yardımcı olma konusunda uzmanlaşmış YZ Botu"
- web_artifact_creator:
- name: "Web Artifact'i Oluşturucu"
- description: "Etkileşimli web artifact'leri oluşturma konusunda uzmanlaşmış YZ Botu"
- custom_prompt:
- name: "Özel istem"
- smart_dates:
- name: "Akıllı tarihler"
- topic_not_found: "Özet mevcut değil, konu bulunamadı!"
- summarizing: "Konu özetleniyor"
- searching: "Aranıyor: '%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "Maksimum sonuç sayısı"
- google:
- base_query:
- name: "Temel Arama Sorgusu"
- description: "Arama yaparken kullanılacak temel sorgu. Örnekler: \"site:example.com\" yalnızca example.com adresindeki sonuçları içerir, before:2022-01-01 yalnızca 2021 ve öncesindeki sonuçları içerir. Bu metin, arama sorgusuna eklenir."
- read:
- read_private:
- name: "Özel oku"
- description: "Kullanıcının erişebildiği tüm konulara erişim izni ver (varsayılan olarak yalnızca herkese açık konular dâhil edilir)"
- search:
- search_private:
- name: "Özel ara"
- description: "Kullanıcının erişebildiği tüm konuları arama sonuçlarına dâhil et (varsayılan olarak yalnızca herkese açık konular dâhil edilir)"
- max_results:
- name: "Maksimum sonuç sayısı"
- description: "Aramaya dahil edilecek maksimum sonuç sayısı; boşsa varsayılan kurallar kullanılır ve kullanılan modele bağlı olarak sayım ölçeklendirilir. En yüksek değer 100'dür."
- base_query:
- name: "Temel Arama Sorgusu"
- description: "Arama yaparken kullanılacak temel sorgu. Örnek: \"#acil\" arama sorgusuna \"#acil\" iadesini ekler ve yalnızca acil kategorisine veya etiketine sahip konuları içerir."
- tool_summary:
- update_artifact: "Web artifact'i güncelle"
- create_artifact: "Web artifact'i oluştur"
- web_browser: "Web'de gezinin"
- github_search_files: "GitHub arama dosyaları"
- github_search_code: "GitHub kodu arama"
- github_file_content: "GitHub dosya içeriği"
- github_pull_request_diff: "GitHub çekme isteği farkı"
- random_picker: "Rastgele Seçici"
- categories: "Kategorileri listele"
- search: "Ara"
- tags: "Etiketleri listele"
- time: "Saat"
- summarize: "Özetle"
- image: "Görüntü oluştur"
- google: "Google'da ara"
- read: "Konuyu oku"
- setting_context: "Site ayarı bağlamına bak"
- schema: "Veri tabanı şemasına bak"
- search_settings: "Site ayarları aranıyor"
- dall_e: "Görüntü oluştur"
- search_meta_discourse: "Meta Discourse'ta arama yapın"
- javascript_evaluator: "JavaScript'i değerlendirin"
- tool_help:
- update_artifact: "YZ Botunu kullanarak bir web artifact'i güncelle"
- create_artifact: "YZ Botunu kullanarak bir web artifact'i oluştur"
- web_browser: "YZ Botunu kullanarak web sayfasına göz atın"
- github_search_code: "GitHub deposunda kod arayın"
- github_search_files: "GitHub deposunda dosya arayın"
- github_file_content: "Bir GitHub deposundan dosyaların içeriğini alın"
- github_pull_request_diff: "GitHub çekme isteği farkını alın"
- random_picker: "Rastgele bir sayı veya listenin rastgele bir ögesini seçin"
- categories: "Forumdaki herkese açık tüm kategorileri listeleyin"
- search: "Forumdaki tüm herkese açık konuları arayın"
- tags: "Forumdaki tüm etiketleri listeleyin"
- time: "Çeşitli saat dilimlerinde zamanı bulun"
- summary: "Bir konuyu özetleyin"
- image: "Stable Diffusion kullanarak görüntü oluşturun"
- google: "Bir sorgu için Google'da arama yapın"
- read: "Forumdaki herkese açık konuyu okuyun"
- setting_context: "Site ayarı bağlamına bakın"
- schema: "Veri tabanı şemasına bakın"
- search_settings: "Site ayarlarında arama yapın"
- dall_e: "DALL-E 3 kullanarak görüntü oluşturun"
- search_meta_discourse: "Meta Discourse'ta arama yapın"
- javascript_evaluator: "JavaScript'i değerlendirin"
- tool_description:
- update_artifact: "YZ Botunu kullanarak bir web artifact'i güncellendi"
- web_browser: "Okunuyor: %{url}"
- github_search_files: "%{repo}/%{branch} içinde \"%{keywords}\" arandı"
- github_search_code: "%{repo} içinde \"%{query}\" arandı"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "%{file_paths} içeriği %{repo_name}@%{branch} kaynağından alındı"
- random_picker: "%{options} adresinden seçiliyor, seçilen: %{result}"
- read: "Okunuyor: %{title}"
- time: "%{timezone} saat diliminde saat %{time}"
- summarize: "%{title} özetlendi"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "%{count} kategori bulundu"
- other: "%{count} kategori bulundu"
- tags:
- one: "%{count} etiket bulundu"
- other: "%{count} etiket bulundu"
- search:
- one: "\"%{query}\" için %{count} sonuç bulundu"
- other: "\"%{query}\" için %{count} sonuç bulundu"
- search_meta_discourse:
- one: "\"%{query}\" için %{count} sonuç bulundu"
- other: "\"%{query}\" için %{count} sonuç bulundu"
- google:
- one: "\"%{query}\" için %{count} sonuç bulundu"
- other: "\"%{query}\" için %{count} sonuç bulundu"
- setting_context: "Şunun için bağlam okunuyor: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "'%{query}' için %{count} sonuç bulundu"
- other: "'%{query}' için %{count} sonuç bulundu"
- summarization:
- configuration_hint:
- one: "Önce `%{setting}` ayarını yapılandırın"
- other: "Önce şu ayarları yapılandırın: %{settings}"
- chat:
- no_targets: "Seçilen dönemde mesaj yok."
- sentiment:
- reports:
- overall_sentiment: "Genel duygu (Olumlu - Olumsuz)"
- post_emotion:
- sadness: "Üzüntü \U0001F622"
- surprise: "Sürpriz \U0001F631"
- neutral: "Nötr \U0001F610"
- fear: "Korku \U0001F628"
- anger: "Öfke \U0001F621"
- joy: "Neşe \U0001F600"
- disgust: "İğrenme \U0001F922"
- sentiment_analysis:
- positive: "Pozitif"
- negative: "Negatif"
- neutral: "Nötr"
- llm:
- configuration:
- disable_module_first: "Önce %{setting} devre dışı bırakılmalı."
- set_llm_first: "Önce %{setting} ayarlanmalı"
- model_unreachable: "Bu modelden yanıt alamadık. Önce ayarlarınızı kontrol edin."
- invalid_seeded_model: "Bu modeli bu özellikle kullanamazsınız"
- must_select_model: "Öncelikle bir LLM seçmelisiniz"
- endpoints:
- not_configured: "%{display_name} (yapılandırılmadı)"
- configuration_hint:
- one: "`%{settings}` ayarının yapılandırıldığından emin olun."
- other: "Şu ayarların yapılandırıldığından emin olun: %{settings}"
- delete_failed:
- one: "%{settings} kullandığı için bu modeli silemedik. Ayarı güncelleyin ve tekrar deneyin."
- other: "%{settings} kullandığı için bu modeli silemedik. Ayarları güncelleyip tekrar deneyin."
- cannot_edit_builtin: "Yerleşik bir modeli düzenleyemezsiniz."
- embeddings:
- delete_failed: "Bu model şu anda kullanımda. Öncelikle `ai embeddings selected model`i güncelleyin."
- cannot_edit_builtin: "Yerleşik bir modeli düzenleyemezsiniz."
- configuration:
- disable_embeddings: "Önce \"yz yerleştirmeleri etkin\" seçeneğini devre dışı bırakmanız gerekiyor."
- invalid_config: "Geçersiz bir seçenek seçtiniz."
- choose_model: "Önce 'ai embeddings selected model'i ayarlayın."
- llm_models:
- missing_provider_param: "%{param} boş olamaz"
- bedrock_invalid_url: "Bu modeli kullanmak için lütfen tüm alanları doldurun."
- ai_staff_action_logger:
- updated: "güncellendi"
- removed: "kaldırıldı"
- errors:
- quota_exceeded: "Bu model için kotayı aştınız. Lütfen %{relative_time} içinde tekrar deneyin."
- quota_required: "Bu model için maksimum token veya kullanımları belirtmelisiniz"
- no_query_specified: Sorgu parametresi gerekli, lütfen belirtin.
- no_user_for_persona: Belirtilen kişilik ile ilişkilendirilmiş bir kullanıcı yok.
- persona_not_found: Belirtilen kişilik mevcut değil. persona_name veya persona_id parametrelerini kontrol edin.
- no_user_specified: Kullanıcı adı veya user_unique_id parametresi gerekli, lütfen belirtin.
- user_not_found: Belirtilen kullanıcı mevcut değil. Kullanıcı adı parametresini kontrol edin.
- persona_disabled: Belirtilen kişilik devre dışı. persona_name veya persona_id parametrelerini kontrol edin.
- no_default_llm: Kişiliğin tanımlanmış bir default_llm'si olmalıdır.
- user_not_allowed: Kullanıcının konuya katılmasına izin verilmiyor.
- prompt_message_length: '%{idx} mesajı 1000 karakter limitinin üzerinde.'
diff --git a/config/locales/server.ug.yml b/config/locales/server.ug.yml
deleted file mode 100644
index 4fbeb22b..00000000
--- a/config/locales/server.ug.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ug:
- reports:
- overall_sentiment:
- yaxis: "چېسلا"
- emotion_neutral:
- title: "\U0001F610 بىتەرەپ"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "ئىزدە"
- tags: "بەلگە تىزىمىنى كۆرسىتىدۇ"
- time: "ۋاقىت"
- summarize: "خۇلاسە"
- summarization:
- chat:
- no_targets: "تاللىغان مەزگىلدىكى ئۇچۇر يوق."
- sentiment:
- reports:
- post_emotion:
- neutral: "بىتەرەپ \U0001F610"
- sentiment_analysis:
- neutral: "بىتەرەپ"
- ai_staff_action_logger:
- updated: "يېڭىلاندى"
- removed: "چىقىرىۋېتىلدى"
diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml
deleted file mode 100644
index 0618fd8c..00000000
--- a/config/locales/server.uk.yml
+++ /dev/null
@@ -1,354 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-uk:
- discourse_automation:
- ai:
- flag_types:
- review: "Додати повідомлення до черги рецензування"
- spam: "Позначити як спам і приховати повідомлення"
- scriptables:
- llm_triage:
- title: Сортування повідомлень за допомогою ШІ
- description: "Сортування повідомлень за допомогою великої мовної моделі"
- llm_report:
- title: Періодичний звіт з використанням ШІ
- description: "Періодичний звіт на основі великої мовної моделі"
- site_settings:
- discourse_ai_enabled: "Увімкнкти плагін ШІ для Discourse"
- ai_toxicity_enabled: "Увімкнути модуль перевірки на токсичність"
- ai_toxicity_inference_service_api_endpoint: "URL-адреса, де працює API для модуля перевірки на токсичність"
- ai_toxicity_inference_service_api_key: "Ключ API для API токсичності"
- ai_toxicity_inference_service_api_model: "Модель для перевірки. Багатомовна модель працює з італійською, французькою, російською, португальською, іспанською та турецькою мовами."
- ai_toxicity_flag_automatically: "Автоматично позначати публікації/повідомлення чату, які перевищують налаштовані порогові значення."
- ai_toxicity_flag_threshold_toxicity: "Токсичність: грубий, неповажний або необґрунтований коментар, який з певною ймовірністю змусить вас вийти з дискусії або відмовитися від обміну думками."
- ai_toxicity_flag_threshold_severe_toxicity: "Сильна токсичність: дуже ненависний, агресивний або неповажний коментар, який з великою ймовірністю змусить вас вийти з дискусії або відмовитися від обміну думками."
- ai_toxicity_flag_threshold_obscene: "Непристойно."
- ai_toxicity_flag_threshold_identity_attack: "Атака на особистість"
- ai_toxicity_flag_threshold_insult: "Образа"
- ai_toxicity_flag_threshold_threat: "Погроза"
- ai_toxicity_flag_threshold_sexual_explicit: "Відверто сексуальний"
- ai_toxicity_groups_bypass: "Користувачі цих груп не матимуть можливості класифікувати свої дописи за модулем токсичності."
- ai_sentiment_enabled: "Увімкніть модуль настроїв."
- ai_sentiment_inference_service_api_endpoint: "URL-адреса, на якій запущено API для модуля настрою"
- ai_sentiment_inference_service_api_key: "Ключ API для API настрою"
- ai_sentiment_models: "Моделі, які можна використовувати для висновків. Почуття класифікує пост у просторі позитивного/нейтрального/негативного. Емоції розпорділяються за категоріями гніву/відрази/страху/радості/нейтральності/суму/подиву."
- ai_nsfw_detection_enabled: "Увімкнути модуль перевірки NSFW."
- ai_nsfw_inference_service_api_endpoint: "URL-адреса, на якій запущено API для модуля NSFW"
- ai_nsfw_inference_service_api_key: "Ключ API для API NSFW"
- ai_nsfw_flag_automatically: "Автоматично позначати публікації NSFW, які перевищують налаштовані порогові значення."
- ai_nsfw_flag_threshold_general: "Загальне порогове значення для зображення, яке буде вважатися NSFW."
- ai_nsfw_flag_threshold_drawings: "Поріг, за яким малюнок вважається NSFW."
- ai_nsfw_flag_threshold_hentai: "Поріг для зображення, класифікованого як хентай, щоб вважатися NSFW."
- ai_nsfw_flag_threshold_porn: "Поріг для зображення, яке класифікується як порно, щоб вважатися NSFW."
- ai_nsfw_flag_threshold_sexy: "Поріг для зображення, класифікованого як сексуальне, щоб вважатися NSFW."
- ai_nsfw_models: "Моделі для використання NSFW умов."
- ai_helper_enabled: "Увімкнути помічника ШІ."
- composer_ai_helper_allowed_groups: "Користувачі цих груп бачитимуть кнопку помічника ШІ в композиторі."
- ai_helper_allowed_in_pm: "Увімкнути Помічник ШІ в особистих повідомленнях."
- ai_helper_model: "Модель для помічника ШІ."
- ai_helper_custom_prompts_allowed_groups: "Користувачі з цих груп побачать опцію користувацьких підказок у помічнику ШІ."
- ai_helper_automatic_chat_thread_title_delay: "Затримка в хвилинах перед тим, як ШІ-помічник автоматично встановить заголовок теми чату."
- ai_helper_automatic_chat_thread_title: "Автоматично встановлювати заголовки тем чату на основі вмісту теми."
- ai_helper_illustrate_post_model: "Модель, яка використовується для ілюстративної функції помічника композитора зі ШІ"
- ai_helper_enabled_features: "Оберіть функції для активації в ШІ-помічнику."
- post_ai_helper_allowed_groups: "Групи користувачів, яким дозволено доступ до функцій ШІ-помічника в публікаціях"
- ai_helper_image_caption_model: "Виберіть модель для генерації підписів до зображень"
- ai_auto_image_caption_allowed_groups: "Користувачі в цих групах можуть вмикати автоматичне додавання підписів до зображень."
- ai_embeddings_generate_for_pms: "Створюйте вбудовування для особистих повідомлень."
- ai_embeddings_semantic_related_topics_enabled: "Використовуйте семантичний пошук для пов’язаних тем."
- ai_embeddings_semantic_related_topics: "Максимальна кількість тем для показу в розділі пов’язаних тем."
- ai_embeddings_backfill_batch_size: "Кількість закладок для заповнення кожні 15 хвилин."
- ai_embeddings_semantic_search_enabled: "Увімкнути повносторінковий семантичний пошук."
- ai_embeddings_semantic_quick_search_enabled: "Увімкнути опцію семантичного пошуку у спливаючому меню пошуку."
- ai_embeddings_semantic_related_include_closed_topics: "Включити закриті теми в результати семантичного пошуку"
- ai_embeddings_semantic_search_hyde_model: "Модель, що використовується для розширення ключових слів для отримання кращих результатів під час семантичного пошуку"
- ai_embeddings_per_post_enabled: Згенерувати вкладення для кожного повідомлення
- ai_summarization_model: "Модель для узагальнення"
- ai_custom_summarization_allowed_groups: "Групи, яким дозволено створювати нові зведення."
- ai_pm_summarization_allowed_groups: "Групи дозволили створювати та переглядати підсумки в особистих повідомленнях."
- ai_summary_gists_allowed_groups: "Групи, яким дозволено бачити суть у списку гарячих тем."
- ai_summary_backfill_maximum_topics_per_hour: "Кількість підсумків тем для заповнення на годину."
- ai_bot_enabled: "Увімкніть модуль AI Bot."
- ai_bot_enable_chat_warning: "Відображати попередження, коли починається чат ПП. Можна змінити, відредагувавши рядок перекладу: discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "Коли GPT-бот отримає доступ до ПП, він відповідатиме членам цих груп."
- ai_bot_debugging_allowed_groups: "Дозвольте цим групам бачити кнопку налагодження в публікаціях, яка відображає необроблений запит і відповідь ШІ"
- ai_bot_public_sharing_allowed_groups: "Дозвольте цим групам ділитися особистими повідомленнями ШІ з громадськістю за унікальним загальнодоступним посиланням. Примітка: якщо ваш сайт вимагає входу, то для доступу до публікацій також потрібно буде ввійти."
- ai_bot_add_to_header: "Відображати кнопку в шапці, щоб почати листування зі ШІ"
- ai_bot_github_access_token: "Токен доступу до GitHub для використання з інструментами штучного інтелекту GitHub (необхідний для підтримки пошуку)"
- ai_stability_api_key: "Ключ API для API Stability.ai"
- ai_stability_engine: "Механізм створення зображень для API Stability.ai"
- ai_stability_api_url: "URL для API Stability.ai"
- ai_google_custom_search_api_key: "Ключ API для Google Custom Search API див. за посиланням: https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "CX для API для пошуку Google"
- reviewables:
- reasons:
- flagged_by_toxicity: Плагін ШІ позначив його, класифікувавши як токсичний.
- flagged_by_nsfw: Плагін AI позначив це після того, як класифікував принаймні одне з прикріплених зображень як NSFW.
- reports:
- overall_sentiment:
- title: "Загальний настрій"
- xaxis: "Позитивні (%)"
- yaxis: "Дата"
- emotion_anger:
- title: "\U0001F620 Гнів"
- emotion_disgust:
- title: "\U0001F922 Огида"
- emotion_fear:
- title: "\U0001F628 Страх"
- emotion_joy:
- title: "\U0001F60A Радість"
- emotion_neutral:
- title: "\U0001F610 Нейтральна"
- emotion_sadness:
- title: "\U0001F62D Смуток"
- emotion_surprise:
- title: "\U0001F632 Сюрприз"
- discourse_ai:
- unknown_model: "Невідома модель ШІ"
- tools:
- custom_name: "%{name} (користувацький)"
- presets:
- browse_web_jina:
- name: "Перегляд веб-сторінок (jina.ai)"
- exchange_rate:
- name: "Обмінний курс"
- stock_quote:
- name: "Котирування акцій (AlphaVantage)"
- image_generation:
- name: "Генератор зображень Flux (Together.ai)"
- empty_tool:
- name: "Почніть з чистого аркуша..."
- ai_helper:
- errors:
- completion_request_failed: "Щось пішло не так під час спроби надати пропозиції. Будь ласка, спробуйте ще раз."
- prompts:
- translate: Перекласти на %{language}
- generate_titles: Запропонувати заголовки тем
- proofread: Перевірити текст
- markdown_table: Створити таблицю у форматі Markdown
- custom_prompt: "Користувацький запит"
- explain: "Пояснити"
- illustrate_post: "Проілюструвати допис"
- painter:
- attribution:
- stable_diffusion_xl: "Зображення від Stable Diffusion XL"
- dall_e_3: "Зображення від DALL-E 3"
- image_caption:
- attribution: "Підпис від ШІ"
- share_ai:
- read_more: "Читати повний вміст"
- onebox_title: "Розмова ШІ з %{llm_name}"
- formatted_excerpt: "Розмова ШІ з %{llm_name}:\n %{excerpt}"
- title: "%{title} – бесіда з ШІ – %{site_name}"
- errors:
- not_allowed: "Вам не дозволено поділитися цією темою"
- other_people_in_pm: "Особисті повідомлення з іншими людьми не можна оприлюднювати"
- other_content_in_pm: "Особисті повідомлення, що містять дописи інших людей, не можна публікувати публічно"
- failed_to_share: "Не вдалося поділитися розмовою"
- conversation_deleted: "Спільний доступ до розмови успішно видалено"
- ai_bot:
- default_pm_prefix: "[ПП від ШІ-бота без назви]"
- personas:
- default_llm_required: "Перед увімкненням чату потрібно встановити модель LLM за замовчуванням"
- cannot_delete_system_persona: "Системні персони не можуть бути видалені, замість цього, будь ласка, вимкніть їх"
- cannot_edit_system_persona: "Системні персони можна лише перейменовувати, ви не можете редагувати інструменти або системні підказки, натомість вимкніть і зробіть копію"
- github_helper:
- name: "Помічник GitHub"
- description: "AI Bot, що спеціалізується на допомозі із завданнями та запитаннями, пов’язаними з GitHub"
- general:
- name: Помічник на форумі
- description: "ШІ-бот загального призначення, здатний виконувати різні завдання"
- artist:
- name: Художник
- description: "AI Bot спеціалізується на створенні зображень"
- sql_helper:
- name: SQL помічник
- description: "AI Bot спеціалізується на допомозі створювати запити SQL для цього екземпляра Discourse"
- settings_explorer:
- name: Провідник налаштувань
- description: "AI Bot спеціалізується на допомозі досліджувати налаштування сайту Discourse"
- creative:
- name: Творчий
- description: "AI Bot без зовнішніх інтеграцій, що спеціалізуються на творчих завданнях"
- dall_e3:
- name: "DALL-E 3"
- description: "AI Bot спеціалізується на створенні зображень за допомогою DALL-E 3"
- discourse_helper:
- name: "Discourse Помічник"
- description: "AI Bot спеціалізується на допомозі із завданнями, пов’язаними з Discourse"
- topic_not_found: "Всього недоступно, тема не знайдена!"
- summarizing: "Підведення підсумків теми"
- searching: "Пошук: '%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "Максимальна кількість результатів"
- google:
- base_query:
- name: "Базовий пошуковий запит"
- description: "Базовий запит для використання під час пошуку. Приклади: 'site:example.com' включатиме лише результати з example.com, before:2022-01-01 включатиме лише результати з 2021 року та раніше. Цей текст додається до пошукового запиту."
- read:
- read_private:
- name: "Читати приват"
- description: "Дозволити доступ до всіх тем, до яких користувач має доступ (за замовчуванням включені лише публічні теми)"
- search:
- search_private:
- name: "Пошук Приватний"
- description: "Включати в результати пошуку всі теми, до яких користувач має доступ (за умовчанням включено лише загальнодоступні теми)"
- max_results:
- name: "Максимальна кількість результатів"
- description: "Максимальна кількість результатів для включення в пошук – якщо порожні, будуть використані правила за замовчуванням, а кількість буде масштабовано залежно від моделі, що використовується. Найвище значення 100."
- base_query:
- name: "Базовий пошуковий запит"
- description: "Базовий запит для пошуку. Приклад: \"#urgent\" додасть \"#urgent\" до пошукового запиту і знайде лише теми з категорією або тегом \"терміново\"."
- tool_summary:
- web_browser: "Перегляд веб-сторінок"
- github_search_files: "Пошук файлів на GitHub"
- github_search_code: "Пошук коду на GitHub"
- github_file_content: "Вміст файлу GitHub"
- github_pull_request_diff: "Порівняння змін GitHub pull request diff"
- random_picker: "Випадковий вибір"
- categories: "Список категорій"
- search: "Пошук"
- tags: "Список тегів"
- time: "Час"
- summarize: "Підсумок"
- image: "Створити зображення"
- google: "Шукати в Google"
- read: "Читати тему"
- setting_context: "Пошук контексту налаштування сайту"
- schema: "Пошук схеми бази даних"
- search_settings: "Пошук налаштувань сайту"
- dall_e: "Створити зображення"
- search_meta_discourse: "Пошук на Meta Discourse"
- javascript_evaluator: "Оцініть JavaScript"
- tool_help:
- web_browser: "Переглядайте веб-сторінки за допомогою AI Bot"
- github_search_code: "Пошук коду в репозиторії GitHub"
- github_search_files: "Пошук файлів у репозиторії GitHub"
- github_file_content: "Отримати вміст файлів з репозиторію GitHub"
- github_pull_request_diff: "Отримати порівняння змін у запиті на GitHub"
- random_picker: "Виберіть випадкове число або випадковий елемент списку"
- categories: "Список усіх категорій для публічного перегляду на форумі"
- search: "Пошук у всіх публічних темах на форумі"
- tags: "Список всіх тегів на форумі"
- time: "Знайти час в різних часових поясах"
- summary: "Підсумувати тему"
- image: "Створити зображення за допомогою Stable Diffusion"
- google: "Пошук запиту в Google"
- read: "Читати публічну тему на форумі"
- setting_context: "Пошук контексту налаштування сайту"
- schema: "Пошук схеми бази даних"
- search_settings: "Пошук налаштувань сайту"
- dall_e: "Створити зображення за допомогою DALL-E 3"
- search_meta_discourse: "Пошук на Meta Discourse"
- javascript_evaluator: "Оцініть JavaScript"
- tool_description:
- web_browser: "Читання %{url}"
- github_search_files: "Шукав «%{keywords}» в %{repo}/%{branch}"
- github_search_code: "Шукав '%{query}' у %{repo}"
- github_pull_request_diff: "%{repo} %{pull_id}%{repo}"
- github_file_content: "Отриманий вміст %{file_paths} з %{repo_name}@%{branch}"
- random_picker: "Вибір із %{options}, вибрано: %{result}"
- read: "Читання: %{title}"
- time: "Час у %{timezone} %{time}"
- summarize: "Підсумок %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- one: "Знайдено %{count} категорію"
- few: "Знайдено %{count} категорії"
- many: "Знайдено %{count} категорій"
- other: "Знайдено %{count} категорій"
- tags:
- one: "Знайдено %{count} тег"
- few: "Знайдено %{count} теги"
- many: "Знайдено %{count} тегів"
- other: "Знайдено %{count} тегів"
- search:
- one: "Знайдено %{count} результат для '%{query}'"
- few: "Знайдено %{count} результати для '%{query}'"
- many: "Знайдено %{count} результатів для '%{query}'"
- other: "Знайдено %{count} результатів для \"%{query}\""
- search_meta_discourse:
- one: "Знайдено %{count} результат для '%{query}'"
- few: "Знайдено %{count} результати для '%{query}'"
- many: "Знайдено %{count} результатів для '%{query}'"
- other: "Знайдено %{count} результатів для \"%{query}\""
- google:
- one: "Знайдено %{count} результат для '%{query}'"
- few: "Знайдено %{count} результати для '%{query}'"
- many: "Знайдено %{count} результатів для '%{query}'"
- other: "Знайдено %{count} результатів для \"%{query}\""
- setting_context: "Читання контексту для: %{setting_name}"
- schema: "%{tables}"
- search_settings:
- one: "Знайдено %{count} результат для '%{query}'"
- few: "Знайдено %{count} результати для '%{query}'"
- many: "Знайдено %{count} результатів для '%{query}'"
- other: "Знайдено %{count} результатів для '%{query}'"
- summarization:
- configuration_hint:
- one: "Спочатку налаштуйте параметр `%{setting}`."
- few: "Спочатку налаштуйте ці параметри: `%{settings}`."
- many: "Спочатку налаштуйте ці параметри: `%{settings}`."
- other: "Спочатку налаштуйте ці параметри: `%{settings}`."
- chat:
- no_targets: "За вибраний період повідомлень не було."
- sentiment:
- reports:
- post_emotion:
- sadness: "Смуток \U0001F622"
- surprise: "Сюрприз \U0001F631"
- neutral: "Нейтральна \U0001F610"
- fear: "Страх \U0001F628"
- anger: "Гнів \U0001F621"
- joy: "Радість \U0001F600"
- disgust: "Огида \U0001F922"
- sentiment_analysis:
- positive: "Позитивний"
- negative: "Негативний"
- neutral: "Нейтральна"
- llm:
- configuration:
- disable_module_first: "Спочатку потрібно вимкнути %{setting} ."
- model_unreachable: "Нам не вдалося отримати відповідь від цієї моделі. Спочатку перевірте свої налаштування."
- invalid_seeded_model: "Ви не можете використовувати цю модель із цією функцією"
- endpoints:
- not_configured: "%{display_name} (не налаштовано)"
- configuration_hint:
- one: "Переконайтеся, що параметр `%{settings}` налаштовано."
- few: "Переконайтеся, що параметри `%{settings}` налаштовано."
- many: "Переконайтеся, що налаштовано параметри: `%{settings}`."
- other: "Переконайтеся, що налаштовано параметри: `%{settings}`."
- delete_failed:
- one: "Ми не змогли видалити цю модель, оскільки її використовує %{settings} . Оновіть налаштування та повторіть спробу."
- few: "Ми не змогли видалити цю модель, оскільки її використовують %{settings} . Оновіть налаштування та повторіть спробу."
- many: "Ми не змогли видалити цю модель, оскільки її використовують %{settings} . Оновіть налаштування та повторіть спробу."
- other: "Ми не змогли видалити цю модель, оскільки її використовують %{settings} . Оновіть налаштування та повторіть спробу."
- cannot_edit_builtin: "Ви не можете редагувати вбудовану модель."
- embeddings:
- cannot_edit_builtin: "Ви не можете редагувати вбудовану модель."
- configuration:
- disable_embeddings: "Спершу потрібно вимкнути функцію «AI embeddings enabled»."
- llm_models:
- missing_provider_param: "%{param} не може бути пустим"
- ai_staff_action_logger:
- updated: "оновлено"
- removed: "вилучені"
- errors:
- no_query_specified: Параметр запиту є обов'язковим, будь ласка, вкажіть його.
- no_user_for_persona: Зазначена персона не має пов'язаного з нею користувача.
- persona_not_found: Вказана персона не існує. Перевірте параметри persona_name або persona_id.
- no_user_specified: Ім'я користувача або параметр user_unique_id є обов'язковим, будь ласка, вкажіть його.
- user_not_found: Зазначений користувач не існує. Перевірте параметр імені користувача.
- persona_disabled: Вказана особа вимкнена. Перевірте параметри persona_name або persona_id.
- no_default_llm: Особа повинна мати значення default_llm.
- user_not_allowed: Користувачеві заборонено брати участь у темі.
- prompt_message_length: Повідомлення %{idx} перевищує ліміт у 1000 символів.
diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml
deleted file mode 100644
index 607292cd..00000000
--- a/config/locales/server.ur.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-ur:
- reports:
- overall_sentiment:
- yaxis: "تاریخ"
- emotion_neutral:
- title: "\U0001F610 نیوٹرل"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "تلاش کریں"
- time: "وقت"
- summarize: "خلاصہ"
- sentiment:
- reports:
- post_emotion:
- neutral: "نیوٹرل \U0001F610"
- sentiment_analysis:
- neutral: "نیوٹرل"
- ai_staff_action_logger:
- updated: "اپ ڈیٹ"
- removed: "ہٹا دیا"
diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml
deleted file mode 100644
index 1929e086..00000000
--- a/config/locales/server.vi.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-vi:
- reports:
- overall_sentiment:
- yaxis: "Ngày"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "Tìm kiếm"
- time: "Thời gian"
- summarize: "Tóm tắt"
- ai_staff_action_logger:
- updated: "đã cập nhật"
- removed: "đã xóa"
diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml
deleted file mode 100644
index 6222a0f3..00000000
--- a/config/locales/server.zh_CN.yml
+++ /dev/null
@@ -1,434 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-zh_CN:
- discourse_automation:
- ai:
- flag_types:
- review: "将帖子添加到审核队列"
- spam: "标记为垃圾内容并隐藏帖子"
- spam_silence: "标记为垃圾内容,隐藏帖子并将用户禁言"
- scriptables:
- llm_triage:
- title: 使用 AI 对帖子进行分类
- description: "使用大语言模型对帖子进行分类"
- flagged_post: |
-
模型的回答:
-
%%LLM_RESPONSE%%
- 由 %%AUTOMATION_NAME%% 规则触发。
- llm_report:
- title: 使用 AI 的定期报告
- description: "基于大语言模型的定期报告"
- site_settings:
- discourse_ai_enabled: "启用 Discourse AI 插件。"
- ai_toxicity_enabled: "启用毒性模块。"
- ai_toxicity_inference_service_api_endpoint: "为毒性模块运行 API 的 URL"
- ai_toxicity_inference_service_api_key: "毒性 API 的 API 密钥"
- ai_toxicity_inference_service_api_model: "用于推断的模型。多语言模型适用于意大利语、法语、俄语、葡萄牙语、西班牙语和土耳其语。"
- ai_toxicity_flag_automatically: "自动举报高于已配置阈值的帖子/聊天消息。"
- ai_toxicity_flag_threshold_toxicity: "毒性:粗鲁、无礼或不合理的评论,可能会让您离开讨论或放弃分享您的观点"
- ai_toxicity_flag_threshold_severe_toxicity: "严重毒性:极度仇恨、激进或无礼的评论,很可能让您离开讨论或放弃分享您的观点"
- ai_toxicity_flag_threshold_obscene: "淫秽"
- ai_toxicity_flag_threshold_identity_attack: "身份攻击"
- ai_toxicity_flag_threshold_insult: "侮辱"
- ai_toxicity_flag_threshold_threat: "威胁"
- ai_toxicity_flag_threshold_sexual_explicit: "露骨色情"
- ai_toxicity_groups_bypass: "这些群组中的用户的帖子不会被毒性模块分类。"
- ai_sentiment_enabled: "启用情绪模块。"
- ai_sentiment_inference_service_api_endpoint: "为情绪模块运行 API 的 URL"
- ai_sentiment_inference_service_api_key: "情绪 API 的 API 密钥"
- ai_sentiment_models: "用于推断的模型。情绪将帖子分类为正面/中性/负面空间。情绪按愤怒/厌恶/恐惧/欢乐/中性/悲伤/惊讶空间进行分类。"
- ai_nsfw_detection_enabled: "启用 NSFW 模块。"
- ai_nsfw_inference_service_api_endpoint: "为 NSFW 模块运行 API 的 URL"
- ai_nsfw_inference_service_api_key: "NSFW API 的 API 密钥"
- ai_nsfw_flag_automatically: "自动举报高于已配置阈值的 NSFW 帖子。"
- ai_nsfw_flag_threshold_general: "图片被视为 NSFW 的一般阈值。"
- ai_nsfw_flag_threshold_drawings: "绘图被视为 NSFW 的阈值。"
- ai_nsfw_flag_threshold_hentai: "分类为成人内容的图片被视为 NSFW 的阈值。"
- ai_nsfw_flag_threshold_porn: "分类为色情作品的图片被视为 NSFW 的阈值。"
- ai_nsfw_flag_threshold_sexy: "分类为性感的图片被视为 NSFW 的阈值。"
- ai_nsfw_models: "用于 NSFW 推断的模型。"
- ai_helper_enabled: "启用 AI 助手。"
- composer_ai_helper_allowed_groups: "这些群组中的用户将在编辑器中看到 AI 助手按钮。"
- ai_helper_allowed_in_pm: "在私信中启用编辑器的 AI 助手。"
- ai_helper_model: "用于 AI 助手的模型。"
- ai_helper_custom_prompts_allowed_groups: "这些群组中的用户将在 AI 助手中看到自定义提示选项。"
- ai_helper_automatic_chat_thread_title_delay: "在 AI 助手自动设置聊天对话标题之前的延迟(以分钟为单位)。"
- ai_helper_automatic_chat_thread_title: "根据话题内容自动设置聊天对话标题。"
- ai_helper_illustrate_post_model: "用于编辑器 AI 助手的为帖子创建插图功能的模型"
- ai_helper_enabled_features: "选择要在 AI 助手中启用的功能。"
- post_ai_helper_allowed_groups: "能够在帖子中访问 AI 助手功能的用户群组"
- ai_helper_image_caption_model: "选择用于生成图片标题的模型"
- ai_auto_image_caption_allowed_groups: "这些群组中的用户可以切换自动生成图片标题。"
- ai_embeddings_selected_model: "使用所选模型生成嵌入向量。"
- ai_embeddings_generate_for_pms: "为个人消息生成嵌入向量。"
- ai_embeddings_semantic_related_topics_enabled: "将语义搜索用于相关话题。"
- ai_embeddings_semantic_related_topics: "相关话题部分中显示的最大话题数。"
- ai_embeddings_backfill_batch_size: "每 15 分钟回填的嵌入向量数。"
- ai_embeddings_semantic_search_enabled: "启用全页语义搜索。"
- ai_embeddings_semantic_quick_search_enabled: "在搜索菜单弹出窗口中启用语义搜索选项。"
- ai_embeddings_semantic_related_include_closed_topics: "在语义搜索结果中包含已关闭话题"
- ai_embeddings_semantic_search_hyde_model: "用于在语义搜索中扩展关键字以获得更好结果的模型"
- ai_embeddings_per_post_enabled: 为每个帖子生成嵌入向量
- ai_custom_summarization_allowed_groups: "能够创建新总结的群组。"
- ai_pm_summarization_allowed_groups: "可以创建摘要并在私信中查看的群组。"
- ai_summary_gists_allowed_groups: "可以查看热门话题列表中的要点的群组。"
- ai_summary_backfill_maximum_topics_per_hour: "每小时回填的话题摘要数。"
- ai_bot_enabled: "启用 AI 机器人模块。"
- ai_bot_enable_chat_warning: "启动私信聊天时显示警告。可以通过编辑翻译字符串进行覆盖:discourse_ai.ai_bot.pm_warning"
- ai_bot_allowed_groups: "当 GPT 机器人可以访问私信时,它将回复这些群组的成员。"
- ai_bot_debugging_allowed_groups: "允许这些群组在帖子上看到显示原始 AI 请求和回答的调试按钮"
- ai_bot_public_sharing_allowed_groups: "允许这些群组通过唯一的公开链接与公众分享 AI 个人消息。注意:如果您的网站需要登录,分享时也需要登录。"
- ai_bot_add_to_header: "在标题中显示按钮以使用 AI 机器人启动私信"
- ai_bot_github_access_token: "使用 GitHub AI 工具所需的 GitHub 访问令牌(搜索支持需要)"
- ai_stability_api_key: "stability.ai API 的 API 密钥"
- ai_stability_engine: "用于 stability.ai API 的图片生成引擎"
- ai_stability_api_url: "stability.ai API 的 URL"
- ai_google_custom_search_api_key: "Google 自定义搜索 API 的 API 密钥,请参阅:https://developers.google.com/custom-search"
- ai_google_custom_search_cx: "Google 自定义搜索 API 的 CX"
- reviewables:
- reasons:
- flagged_by_toxicity: AI 插件将其分类为有毒后对其进行了举报。
- flagged_by_nsfw: AI 插件在将至少一张附加图片分类为 NSFW 后对其进行了举报。
- reports:
- overall_sentiment:
- title: "整体情绪"
- description: '该图表会比较分类为积极或消极的帖子数。当积极分数或消极分数大于设定的阈值分数时,会计算这些值。这意味着不显示中性的帖子。私信 (PM) 也不包括在内。使用“cardiffnlp/twitter-roberta-base-sentiment-latest”进行分类'
- xaxis: "积极 (%)"
- yaxis: "日期"
- emotion_admiration:
- title: "\U0001F929 钦佩"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为钦佩情绪的帖子。"
- emotion_amusement:
- title: "\U0001F604 愉悦"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为愉悦情绪的帖子。"
- emotion_anger:
- title: "\U0001F620 愤怒"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为愤怒情绪的帖子。"
- emotion_annoyance:
- title: "\U0001F612 烦恼"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为烦恼情绪的帖子。"
- emotion_approval:
- title: "\U0001F44D 赞同"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为赞同情绪的帖子。"
- emotion_caring:
- title: "\U0001F917 关怀"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为关怀情绪的帖子。"
- emotion_confusion:
- title: "\U0001F615 困惑"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为困惑情绪的帖子。"
- emotion_curiosity:
- title: "\U0001F914 好奇"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为好奇情绪的帖子。"
- emotion_desire:
- title: "\U0001F60D 渴望"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为渴望情绪的帖子。"
- emotion_disappointment:
- title: "\U0001F61E 沮丧"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为沮丧情绪的帖子。"
- emotion_disapproval:
- title: "\U0001F44E 不赞同"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为不赞同情绪的帖子。"
- emotion_disgust:
- title: "\U0001F922 厌恶"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为厌恶情绪的帖子。"
- emotion_embarrassment:
- title: "\U0001F633 尴尬"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为尴尬情绪的帖子。"
- emotion_excitement:
- title: "\U0001F92A 兴奋"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为兴奋情绪的帖子。"
- emotion_fear:
- title: "\U0001F628 恐惧"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为恐惧情绪的帖子。"
- emotion_gratitude:
- title: "\U0001F64F 感谢"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为感谢情绪的帖子。"
- emotion_grief:
- title: "\U0001F622 悲伤"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为悲伤情绪的帖子。"
- emotion_joy:
- title: "\U0001F60A 开心"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为开心情绪的帖子。"
- emotion_love:
- title: '❤️ 爱心'
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为爱心情绪的帖子。"
- emotion_nervousness:
- title: "\U0001F630 紧张"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为紧张情绪的帖子。"
- emotion_neutral:
- title: "\U0001F610 中性"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为中性情绪的帖子。"
- emotion_optimism:
- title: "\U0001F31F 乐观"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为乐观情绪的帖子。"
- emotion_pride:
- title: "\U0001F981 骄傲"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为骄傲情绪的帖子。"
- emotion_realization:
- title: "\U0001F4A1 领悟"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为领悟情绪的帖子。"
- emotion_relief:
- title: "\U0001F60C 缓解"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为缓解情绪的帖子。"
- emotion_remorse:
- title: "\U0001F614 懊悔"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为懊悔情绪的帖子。"
- emotion_sadness:
- title: "\U0001F62D 悲伤"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为悲伤情绪的帖子。"
- emotion_surprise:
- title: "\U0001F632 惊讶"
- description: "使用“SamLowe/roberta-base-go_emotions”模型,通过 AI 分类为惊讶情绪的帖子。"
- discourse_ai:
- ai_artifact:
- view_source: "查看来源"
- view_changes: "查看更改"
- unknown_model: "未知 AI 模型"
- tools:
- custom_name: "%{name}(自定义)"
- presets:
- browse_web_jina:
- name: "浏览网页 (jina.ai)"
- exchange_rate:
- name: "汇率"
- stock_quote:
- name: "股票报价 (AlphaVantage)"
- image_generation:
- name: "Flux 图片生成器 (Together.ai)"
- empty_tool:
- name: "从空白开始…"
- ai_helper:
- errors:
- completion_request_failed: "尝试提供建议时出错。请重试。"
- prompts:
- translate: 翻译为%{language}
- generate_titles: 建议话题标题
- proofread: 审校文本
- markdown_table: 生成 Markdown 表
- custom_prompt: "自定义提示"
- explain: "解释"
- illustrate_post: "为帖子创建插图"
- replace_dates: "智能日期"
- painter:
- attribution:
- stable_diffusion_xl: "图片由 Stable Diffusion XL 提供"
- dall_e_3: "图片由 DALL-E 3 提供"
- image_caption:
- attribution: "由 AI 生成标题"
- share_ai:
- read_more: "阅读完整转写文稿"
- onebox_title: "与 %{llm_name} 的 AI 对话"
- formatted_excerpt: "与 %{llm_name} 的 AI 对话:\n %{excerpt}"
- title: "%{title} - AI 对话 - %{site_name}"
- errors:
- not_allowed: "您无权分享此话题"
- other_people_in_pm: "不能公开分享与其他人的个人消息"
- other_content_in_pm: "不能公开分享包含其他人发表的帖子的个人消息"
- failed_to_share: "无法分享对话"
- conversation_deleted: "对话分享已成功删除"
- spam_detection:
- flag_reason: "被 Discourse AI 标记为垃圾内容"
- silence_reason: "用户被 Discourse AI 自动禁言"
- invalid_error_type: "提供的错误类型无效"
- unexpected: "发生意外错误"
- bot_user_update_failed: "无法更新垃圾内容扫描机器人用户"
- ai_bot:
- reply_error: "抱歉,我们的系统在尝试回复时似乎遇到了意外问题。\n\n[details='Error details']\n%{details}\n[/details]"
- default_pm_prefix: "[无标题 AI 机器人私信]"
- personas:
- default_llm_required: "启用聊天之前需要默认的 LLM 模型"
- cannot_delete_system_persona: "无法删除系统角色,请改为将其禁用"
- cannot_edit_system_persona: "系统角色只能重命名,您不能编辑工具或系统提示,但可以禁用和复制"
- github_helper:
- name: "GitHub 助手"
- description: "专门协助处理 GitHub 相关任务和问题的 AI 机器人"
- general:
- name: 论坛助手
- description: "能够执行各种任务的通用 AI 机器人"
- artist:
- name: 艺术家
- description: "专门用于生成图片的 AI 机器人"
- sql_helper:
- name: SQL 助手
- description: "专门帮助在此 Discourse 实例上编写 SQL 查询的 AI 机器人"
- settings_explorer:
- name: 设置资源管理器
- description: "专门帮助探索 Discourse 站点设置的 AI 机器人"
- creative:
- name: 创作者
- description: "无需外部集成的专门从事创作任务的 AI 机器人"
- dall_e3:
- name: "DALL-E 3"
- description: "专门使用 DALL-E 3 生成图片的 AI 机器人"
- discourse_helper:
- name: "Discourse 助手"
- description: "专门帮助处理 Discourse 相关任务的 AI 机器人"
- web_artifact_creator:
- name: "Web 工件创建者"
- description: "专注于创建交互式 Web 工件的 AI 机器人"
- custom_prompt:
- name: "自定义提示"
- smart_dates:
- name: "智能日期"
- topic_not_found: "总结不可用,找不到话题!"
- summarizing: "正在总结话题"
- searching: "搜索:'%{query}'"
- tool_options:
- researcher:
- max_results:
- name: "最大结果数"
- google:
- base_query:
- name: "基本搜索查询"
- description: "搜索时使用的基本查询。例如:'site:example.com' 将仅包括 example.com 中的结果,before:2022-01-01 将仅包括 2021 年及之前的结果。此文本会附加到搜索查询的前面。"
- read:
- read_private:
- name: "阅读不公开话题"
- description: "允许访问用户可以访问的所有话题(默认情况下仅包含公开话题)"
- search:
- search_private:
- name: "搜索不公开话题"
- description: "在搜索结果中包含用户可以访问的所有话题(默认情况下仅包含公开话题)"
- max_results:
- name: "最大结果数"
- description: "要包含在搜索中的最大结果数 – 如果留空,将使用默认规则,并且计数将根据使用的模型进行调整。最大值为 100。"
- base_query:
- name: "基本搜索查询"
- description: "搜索时要使用的基本查询。示例:'#urgent' 会在搜索查询前面加上 '#urgent',并且仅包含具有紧急类别或标签的话题。"
- tool_summary:
- update_artifact: "更新 Web 工件"
- create_artifact: "创建 Web 工件"
- web_browser: "浏览网页"
- github_search_files: "GitHub 搜索文件"
- github_search_code: "GitHub 代码搜索"
- github_file_content: "GitHub 文件内容"
- github_pull_request_diff: "GitHub 拉取请求差异"
- random_picker: "随机选择器"
- categories: "列出类别"
- search: "搜索"
- tags: "列出标签"
- time: "时间"
- summarize: "总结"
- image: "生成图片"
- google: "搜索 Google"
- read: "阅读话题"
- setting_context: "查找网站设置上下文"
- schema: "查找数据库架构"
- search_settings: "正在搜索网站设置"
- dall_e: "生成图片"
- search_meta_discourse: "搜索元 Discourse"
- javascript_evaluator: "评估 JavaScript"
- tool_help:
- update_artifact: "使用 AI 机器人更新 Web 工件"
- create_artifact: "使用 AI 机器人创建 Web 工件"
- web_browser: "使用 AI 机器人浏览网页"
- github_search_code: "在 GitHub 仓库中搜索代码"
- github_search_files: "在 GitHub 仓库中搜索文件"
- github_file_content: "从 GitHub 仓库检索文件内容"
- github_pull_request_diff: "检索 GitHub 拉取请求差异"
- random_picker: "选择一个随机数或列表的随机元素"
- categories: "列出论坛上所有公开可见的类别"
- search: "搜索论坛上的所有公开话题"
- tags: "列出论坛上的所有标签"
- time: "查找不同时区的时间"
- summary: "总结话题"
- image: "使用 Stable Diffusion 生成图片"
- google: "在 Google 中搜索查询"
- read: "在论坛上阅读公开话题"
- setting_context: "查找网站设置上下文"
- schema: "查找数据库架构"
- search_settings: "搜索网站设置"
- dall_e: "使用 DALL-E 3 生成图片"
- search_meta_discourse: "搜索元 Discourse"
- javascript_evaluator: "评估 JavaScript"
- tool_description:
- update_artifact: "已使用 AI 机器人更新 Web 工件"
- web_browser: "正在阅读 %{url}"
- github_search_files: "已在 %{repo}/%{branch} 中搜索 '%{keywords}'"
- github_search_code: "已在 %{repo} 中搜索 '%{query}'"
- github_pull_request_diff: "%{repo} %{pull_id}"
- github_file_content: "已从 %{repo_name}@%{branch} 检索到 %{file_paths} 的内容"
- random_picker: "正在从 %{options} 中选择,已选:%{result}"
- read: "正在阅读:%{title}"
- time: "%{timezone} 的时间为 %{time}"
- summarize: "已总结 %{title}"
- dall_e: "%{prompt}"
- create_image: "%{prompt}"
- edit_image: "%{prompt}"
- image: "%{prompt}"
- categories:
- other: "找到 %{count} 个类别"
- tags:
- other: "找到 %{count} 个标签"
- search:
- other: "找到 '%{query}' 的 %{count} 个结果"
- search_meta_discourse:
- other: "找到 '%{query}' 的 %{count} 个结果"
- google:
- other: "找到 '%{query}' 的 %{count} 个结果"
- setting_context: "正在阅读上下文:%{setting_name}"
- schema: "%{tables}"
- search_settings:
- other: "找到“%{query}”的 %{count} 个结果"
- summarization:
- configuration_hint:
- other: "首先配置这些设置:%{settings}"
- chat:
- no_targets: "在所选时间段内没有消息。"
- sentiment:
- reports:
- overall_sentiment: "整体情绪(积极 - 消极)"
- post_emotion:
- sadness: "悲伤 \U0001F622"
- surprise: "惊讶 \U0001F631"
- neutral: "中性 \U0001F610"
- fear: "恐惧 \U0001F628"
- anger: "愤怒 \U0001F621"
- joy: "开心\U0001F600"
- disgust: "厌恶 \U0001F922"
- sentiment_analysis:
- positive: "积极"
- negative: "消极"
- neutral: "中性"
- llm:
- configuration:
- disable_module_first: "您必须先禁用“%{setting}”。"
- set_llm_first: "首先设置%{setting}"
- model_unreachable: "我们无法从此模型获得回应。请先检查您的设置。"
- invalid_seeded_model: "您无法在此模型中使用此功能"
- must_select_model: "必须先选择一个 LLM"
- endpoints:
- not_configured: "%{display_name}(未配置)"
- configuration_hint:
- other: "确保配置了以下设置:%{settings}"
- delete_failed:
- other: "由于 %{settings} 正在使用此模型,无法将其删除。请更新设置并重试。"
- cannot_edit_builtin: "您无法编辑内置模型。"
- embeddings:
- delete_failed: "此模型目前正在使用中。请先更新 `ai embeddings selected model`。"
- cannot_edit_builtin: "您无法编辑内置模型。"
- configuration:
- disable_embeddings: "您必须先禁用 'ai embeddings enabled'。"
- choose_model: "首先设置 'ai embeddings selected model'。"
- llm_models:
- missing_provider_param: "%{param} 不能为空"
- bedrock_invalid_url: "请填写所有字段以使用此模型。"
- ai_staff_action_logger:
- updated: "更新"
- removed: "已移除"
- errors:
- quota_exceeded: "您已超出此模型的配额。请在 %{relative_time}后再试。"
- quota_required: "必须为此模型指定最大词元数或使用次数"
- no_query_specified: 查询参数为必填项,请指定。
- no_user_for_persona: 指定的角色没有与之关联的用户。
- persona_not_found: 指定的角色不存在。请检查 persona_name 或 persona_id 参数。
- no_user_specified: username 或 user_unique_id 参数为必填项,请指定。
- user_not_found: 指定的用户不存在。请检查 username 参数。
- persona_disabled: 指定的角色被禁用。请检查 persona_name 或 persona_id 参数。
- no_default_llm: 角色必须定义一个 default_llm。
- user_not_allowed: 该用户无法参与该话题。
- prompt_message_length: 消息 %{idx} 超过 1000 个字符限制。
diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml
deleted file mode 100644
index 6e592f3a..00000000
--- a/config/locales/server.zh_TW.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-# WARNING: Never edit this file.
-# It will be overwritten when translations are pulled from Crowdin.
-#
-# To work with us on translations, join this project:
-# https://translate.discourse.org/
-
-zh_TW:
- reports:
- overall_sentiment:
- yaxis: "日期"
- emotion_neutral:
- title: "\U0001F610 中性"
- discourse_ai:
- ai_bot:
- tool_summary:
- search: "搜尋"
- time: "時間"
- summarize: "總結"
- sentiment:
- reports:
- post_emotion:
- neutral: "中性 \U0001F610"
- sentiment_analysis:
- neutral: "中性"
- ai_staff_action_logger:
- updated: "更新時間"
- removed: "已移除"
diff --git a/config/routes.rb b/config/routes.rb
deleted file mode 100644
index d3ea4497..00000000
--- a/config/routes.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-# frozen_string_literal: true
-
-DiscourseAi::Engine.routes.draw do
- scope module: :ai_helper, path: "/ai-helper", defaults: { format: :json } do
- post "suggest" => "assistant#suggest"
- post "suggest_title" => "assistant#suggest_title"
- post "suggest_category" => "assistant#suggest_category"
- post "suggest_tags" => "assistant#suggest_tags"
- post "stream_suggestion" => "assistant#stream_suggestion"
- post "caption_image" => "assistant#caption_image"
- end
-
- scope module: :embeddings, path: "/embeddings", defaults: { format: :json } do
- get "semantic-search" => "embeddings#search"
- get "quick-search" => "embeddings#quick_search"
- end
-
- scope module: :discord, path: "/discord", defaults: { format: :json } do
- post "interactions" => "bot#interactions"
- end
-
- scope module: :ai_bot, path: "/ai-bot", defaults: { format: :json } do
- get "bot-username" => "bot#show_bot_username"
- get "post/:post_id/show-debug-info" => "bot#show_debug_info"
- get "show-debug-info/:id" => "bot#show_debug_info_by_id"
- post "post/:post_id/stop-streaming" => "bot#stop_streaming_response"
-
- get "discover" => "bot#discover"
- post "discover/continue-convo" => "bot#discover_continue_convo"
- end
-
- scope module: :ai_bot, path: "/ai-bot/shared-ai-conversations" do
- post "/" => "shared_ai_conversations#create"
- delete "/:share_key" => "shared_ai_conversations#destroy"
- get "/:share_key" => "shared_ai_conversations#show"
- get "/asset/:version/:name" => "shared_ai_conversations#asset"
- get "/preview/:topic_id" => "shared_ai_conversations#preview"
- end
-
- scope module: :ai_bot, path: "/ai-bot/conversations" do
- get "/" => "conversations#index"
- end
-
- scope module: :ai_bot, path: "/ai-bot/artifacts" do
- get "/:id" => "artifacts#show"
- get "/:id/:version" => "artifacts#show"
- end
-
- scope module: :ai_bot, path: "/ai-bot/artifact-key-values/:artifact_id" do
- get "/" => "artifact_key_values#index"
- post "/" => "artifact_key_values#set"
- delete "/:key" => "artifact_key_values#destroy"
- delete "/" => "artifact_key_values#destroy"
- end
-
- scope module: :summarization, path: "/summarization", defaults: { format: :json } do
- get "/t/:topic_id" => "summary#show", :constraints => { topic_id: /\d+/ }
- get "/channels/:channel_id" => "chat_summary#show"
- end
-
- scope module: :sentiment, path: "/sentiment", defaults: { format: :json } do
- get "/posts" => "sentiment#posts", :constraints => StaffConstraint.new
- end
-end
-
-Discourse::Application.routes.draw do
- mount ::DiscourseAi::Engine, at: "discourse-ai"
-
- get "admin/dashboard/sentiment" => "discourse_ai/admin/dashboard#sentiment",
- :constraints => StaffConstraint.new
-
- scope "/admin/plugins/discourse-ai", constraints: AdminConstraint.new do
- resources :ai_personas,
- only: %i[index new create edit update destroy],
- path: "ai-personas",
- controller: "discourse_ai/admin/ai_personas"
-
- post "/ai-personas/stream-reply" => "discourse_ai/admin/ai_personas#stream_reply"
-
- resources(
- :ai_tools,
- only: %i[index new create edit update destroy],
- path: "ai-tools",
- controller: "discourse_ai/admin/ai_tools",
- )
-
- post "/ai-tools/:id/test", to: "discourse_ai/admin/ai_tools#test"
- get "/ai-tools/:id/export", to: "discourse_ai/admin/ai_tools#export", format: :json
- post "/ai-tools/import", to: "discourse_ai/admin/ai_tools#import"
-
- post "/ai-personas/:id/create-user", to: "discourse_ai/admin/ai_personas#create_user"
- get "/ai-personas/:id/export", to: "discourse_ai/admin/ai_personas#export", format: :json
- post "/ai-personas/import", to: "discourse_ai/admin/ai_personas#import"
-
- put "/ai-personas/:id/files/remove", to: "discourse_ai/admin/ai_personas#remove_file"
- get "/ai-personas/:id/files/status", to: "discourse_ai/admin/ai_personas#indexing_status_check"
-
- post "/rag-document-fragments/files/upload",
- to: "discourse_ai/admin/rag_document_fragments#upload_file"
- get "/rag-document-fragments/files/status",
- to: "discourse_ai/admin/rag_document_fragments#indexing_status_check"
-
- get "/ai-usage", to: "discourse_ai/admin/ai_usage#show"
- get "/ai-usage-report", to: "discourse_ai/admin/ai_usage#report"
- get "/ai-spam", to: "discourse_ai/admin/ai_spam#show"
- put "/ai-spam", to: "discourse_ai/admin/ai_spam#update"
- post "/ai-spam/test", to: "discourse_ai/admin/ai_spam#test"
- post "/ai-spam/fix-errors", to: "discourse_ai/admin/ai_spam#fix_errors"
-
- resources :ai_llms,
- only: %i[index new create edit update destroy],
- path: "ai-llms",
- controller: "discourse_ai/admin/ai_llms" do
- collection { get :test }
- end
-
- resources :ai_llm_quotas,
- controller: "discourse_ai/admin/ai_llm_quotas",
- path: "quotas",
- only: %i[index create update destroy]
-
- resources :ai_embeddings,
- only: %i[index new create edit update destroy],
- path: "ai-embeddings",
- controller: "discourse_ai/admin/ai_embeddings" do
- collection { get :test }
- end
-
- resources :ai_features,
- only: %i[index edit],
- path: "ai-features",
- controller: "discourse_ai/admin/ai_features"
- end
-end
-
-Discourse::Application.routes.append do
- get "u/:username/preferences/ai" => "users#preferences",
- :constraints => {
- username: RouteFormat.username,
- }
-end
diff --git a/config/settings.yml b/config/settings.yml
deleted file mode 100644
index 43646ff6..00000000
--- a/config/settings.yml
+++ /dev/null
@@ -1,596 +0,0 @@
-discourse_ai:
- discourse_ai_enabled:
- default: false
- client: true
- ai_artifact_security:
- client: true
- type: enum
- default: "strict"
- choices:
- - "disabled"
- - "lax"
- - "hybrid"
- - "strict"
-
- ai_sentiment_enabled:
- default: false
- client: true
- ai_sentiment_model_configs:
- default: ""
- json_schema: DiscourseAi::Sentiment::SentimentSiteSettingJsonSchema
- ai_sentiment_backfill_maximum_posts_per_hour:
- default: 2500
- min: 0
- max: 10000
- hidden: true
- ai_sentiment_backfill_post_max_age_days:
- default: 60
- hidden: true
-
- ai_openai_image_generation_url: "https://api.openai.com/v1/images/generations"
- ai_openai_image_edit_url: "https://api.openai.com/v1/images/edits"
- ai_openai_embeddings_url:
- hidden: true
- default: "https://api.openai.com/v1/embeddings"
- ai_openai_organization:
- default: ""
- hidden: true
- ai_openai_api_key:
- default: ""
- secret: true
- ai_stability_api_key:
- default: ""
- secret: true
- ai_stability_api_url:
- default: "https://api.stability.ai"
- ai_stability_engine:
- default: "stable-diffusion-xl-1024-v1-0"
- type: enum
- choices:
- - "sd3"
- - "sd3-turbo"
- - "stable-diffusion-xl-1024-v1-0"
- - "stable-diffusion-768-v2-1"
- - "stable-diffusion-v1-5"
- ai_hugging_face_tei_endpoint:
- hidden: true
- default: ""
- ai_hugging_face_tei_endpoint_srv:
- default: ""
- hidden: true
- ai_hugging_face_tei_api_key:
- default: ""
- hidden: true
- ai_hugging_face_tei_reranker_endpoint:
- default: ""
- ai_hugging_face_tei_reranker_endpoint_srv:
- default: ""
- hidden: true
- ai_hugging_face_tei_reranker_api_key: ""
- ai_google_custom_search_api_key:
- default: ""
- secret: true
- ai_google_custom_search_cx:
- default: ""
- ai_cloudflare_workers_account_id:
- default: ""
- secret: true
- hidden: true
- ai_cloudflare_workers_api_token:
- default: ""
- secret: true
- hidden: true
- ai_gemini_api_key:
- default: ""
- hidden: true
- ai_strict_token_counting:
- default: false
- hidden: true
-
- ai_helper_enabled:
- default: false
- client: true
- validator: "DiscourseAi::Configuration::LlmDependencyValidator"
- area: "ai-features/ai_helper"
- composer_ai_helper_allowed_groups:
- type: group_list
- list_type: compact
- default: "3|14" # 3: @staff, 14: @trust_level_4
- allow_any: false
- refresh: true
- area: "ai-features/ai_helper"
- ai_helper_allowed_in_pm:
- default: false
- client: true
- area: "ai-features/ai_helper"
- ai_helper_model:
- default: ""
- allow_any: false
- type: enum
- enum: "DiscourseAi::Configuration::LlmEnumerator"
- hidden: true
- ai_helper_custom_prompts_allowed_groups: # Deprecated. TODO(roman): Remove 2025-09-01
- type: group_list
- list_type: compact
- default: "3" # 3: @staff
- allow_any: false
- refresh: true
- hidden: true
- post_ai_helper_allowed_groups:
- type: group_list
- list_type: compact
- default: "3|14" # 3: @staff, 14: @trust_level_4
- allow_any: false
- refresh: true
- area: "ai-features/ai_helper"
- ai_helper_automatic_chat_thread_title:
- default: false
- area: "ai-features/ai_helper"
- ai_helper_automatic_chat_thread_title_delay:
- default: 5
- area: "ai-features/ai_helper"
- ai_helper_illustrate_post_model:
- default: disabled
- type: enum
- choices:
- - stable_diffusion_xl
- - dall_e_3
- - disabled
- area: "ai-features/ai_helper"
- ai_helper_enabled_features:
- client: true
- default: "suggestions|context_menu"
- type: list
- list_type: compact
- allow_any: false
- refresh: true
- choices:
- - "suggestions"
- - "context_menu"
- - "image_caption"
- area: "ai-features/ai_helper"
- ai_helper_image_caption_model:
- default: ""
- type: enum
- enum: "DiscourseAi::Configuration::LlmVisionEnumerator"
- hidden: true
- ai_auto_image_caption_allowed_groups:
- client: true
- type: group_list
- list_type: compact
- default: "10" # 10: @trust_level_0
- allow_any: false
- refresh: true
- area: "ai-features/ai_helper"
- ai_helper_model_allowed_seeded_models:
- default: ""
- hidden: true
- type: list
- list_type: compact
- ai_helper_image_caption_model_allowed_seeded_models:
- default: ""
- hidden: true
- type: list
- list_type: compact
- ai_helper_proofreader_persona:
- default: "-22"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/ai_helper"
- ai_helper_title_suggestions_persona:
- default: "-23"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/ai_helper"
- ai_helper_explain_persona:
- default: "-24"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/ai_helper"
- ai_helper_post_illustrator_persona:
- default: "-21"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/ai_helper"
- ai_helper_smart_dates_persona:
- default: "-19"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/ai_helper"
- ai_helper_translator_persona:
- default: "-25"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/ai_helper"
- ai_helper_markdown_tables_persona:
- default: "-20"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/ai_helper"
- ai_helper_custom_prompt_persona:
- default: "-18"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/ai_helper"
- ai_helper_image_caption_persona:
- default: "-26"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/ai_helper"
-
- ai_embeddings_enabled:
- default: false
- client: true
- validator: "DiscourseAi::Configuration::EmbeddingsModuleValidator"
- area: "ai-features/embeddings"
- ai_embeddings_selected_model:
- type: enum
- default: ""
- allow_any: false
- enum: "DiscourseAi::Configuration::EmbeddingDefsEnumerator"
- validator: "DiscourseAi::Configuration::EmbeddingDefsValidator"
- area: "ai-features/embeddings"
- ai_embeddings_backfill_model:
- type: enum
- default: ""
- allow_any: false
- enum: "DiscourseAi::Configuration::EmbeddingDefsEnumerator"
- hidden: true
- ai_embeddings_per_post_enabled:
- default: false
- hidden: true
- ai_embeddings_generate_for_pms:
- default: false
- area: "ai-features/embeddings"
- ai_embeddings_semantic_related_topics_enabled:
- default: false
- client: true
- area: "ai-features/embeddings"
- ai_embeddings_semantic_related_topics:
- default: 5
- area: "ai-features/embeddings"
- ai_embeddings_semantic_related_include_closed_topics:
- default: true
- area: "ai-features/embeddings"
- ai_embeddings_backfill_batch_size:
- default: 250
- hidden: true
- ai_embeddings_semantic_search_enabled:
- default: false
- client: true
- validator: "DiscourseAi::Configuration::LlmDependencyValidator"
- area: "ai-features/embeddings"
- ai_embeddings_semantic_search_hyde_model:
- default: ""
- type: enum
- allow_any: false
- enum: "DiscourseAi::Configuration::LlmEnumerator"
- validator: "DiscourseAi::Configuration::LlmValidator"
- area: "ai-features/embeddings"
- ai_embeddings_semantic_search_hyde_model_allowed_seeded_models:
- default: ""
- hidden: true
- type: list
- list_type: compact
- ai_embeddings_semantic_quick_search_enabled:
- default: false
- client: true
- hidden: true
- area: "ai-features/embeddings"
- ai_embeddings_semantic_search_hyde_persona:
- default: "-32"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/embeddings"
-
- ai_embeddings_discourse_service_api_endpoint:
- default: ""
- hidden: true
- ai_embeddings_discourse_service_api_endpoint_srv:
- default: ""
- hidden: true
- ai_embeddings_discourse_service_api_key:
- hidden: true
- default: ""
- secret: true
- ai_embeddings_model:
- hidden: true
- type: enum
- default: "bge-large-en"
- allow_any: false
- choices:
- - all-mpnet-base-v2
- - text-embedding-ada-002
- - text-embedding-3-small
- - text-embedding-3-large
- - multilingual-e5-large
- - bge-large-en
- - gemini
- - bge-m3
- ai_embeddings_pg_connection_string:
- default: ""
- hidden: true
-
- ai_summarization_enabled:
- default: false
- client: true
- validator: "DiscourseAi::Configuration::LlmDependencyValidator"
- area: "ai-features/summarization"
- ai_summarization_model:
- default: ""
- allow_any: false
- type: enum
- enum: "DiscourseAi::Configuration::LlmEnumerator"
- validator: "DiscourseAi::Configuration::LlmValidator"
- hidden: true
- ai_summarization_persona:
- default: "-11"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/summarization"
- ai_pm_summarization_allowed_groups:
- type: group_list
- list_type: compact
- default: ""
- area: "ai-features/summarization"
- ai_custom_summarization_allowed_groups: # Deprecated. TODO(roman): Remove 2025-09-01
- type: group_list
- list_type: compact
- default: "3|13" # 3: @staff, 13: @trust_level_3
- hidden: true
- ai_summary_gists_enabled:
- default: false
- area: "ai-features/summarization"
- ai_summary_gists_persona:
- default: "-12"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/summarization"
- ai_summary_gists_allowed_groups: # Deprecated. TODO(roman): Remove 2025-09-01
- type: group_list
- list_type: compact
- default: "0" #everyone
- hidden: true
- ai_summarization_model_allowed_seeded_models:
- default: ""
- hidden: true
- type: list
- list_type: compact
- ai_summary_backfill_topic_max_age_days:
- default: 30
- min: 1
- max: 10000
- area: "ai-features/summarization"
- ai_summary_backfill_maximum_topics_per_hour:
- default: 0
- min: 0
- max: 10000
- area: "ai-features/summarization"
- ai_summary_backfill_minimum_word_count:
- default: 200
- area: "ai-features/summarization"
-
- ai_bot_enabled:
- default: false
- client: true
- area: "ai-features/bot"
- ai_bot_enable_chat_warning:
- default: false
- client: true
- area: "ai-features/bot"
- ai_bot_debugging_allowed_groups:
- type: group_list
- list_type: compact
- default: ""
- allow_any: false
- area: "ai-features/bot"
- ai_bot_allowed_groups:
- type: group_list
- list_type: compact
- default: "3|14" # 3: @staff, 14: @trust_level_4
- area: "ai-features/bot"
- ai_bot_public_sharing_allowed_groups:
- client: false
- type: group_list
- list_type: compact
- default: "1|2" # 1: admins, 2: moderators
- allow_any: false
- refresh: true
- area: "ai-features/bot"
- ai_bot_add_to_header:
- default: true
- client: true
- area: "ai-features/bot"
- ai_bot_github_access_token:
- default: ""
- secret: true
- area: "ai-features/bot"
- ai_bot_allowed_seeded_models:
- default: ""
- hidden: true
- type: list
- list_type: compact
- area: "ai-features/bot"
- ai_bot_discover_persona:
- default: ""
- type: enum
- client: true
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/search"
- ai_automation_max_triage_per_minute:
- default: 60
- hidden: true
- ai_automation_max_triage_per_post_per_minute:
- default: 2
- hidden: true
- ai_automation_allowed_seeded_models:
- default: ""
- hidden: true
- type: list
- list_type: compact
-
- ai_discord_search_enabled:
- default: false
- client: true
- area: "ai-features/discord"
- ai_discord_app_id:
- default: ""
- client: false
- area: "ai-features/discord"
- ai_discord_app_public_key:
- default: ""
- client: false
- area: "ai-features/discord"
- ai_discord_search_mode:
- default: "search"
- type: enum
- choices:
- - search
- - persona
- area: "ai-features/discord"
- ai_discord_search_persona:
- default: ""
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/discord"
- ai_discord_allowed_guilds:
- type: list
- list_type: compact
- default: ""
- area: "ai-features/discord"
-
- ai_spam_detection_enabled:
- default: false
- validator: "DiscourseAi::Configuration::SpamDetectionValidator"
- ai_spam_detection_user_id:
- default: ""
- hidden: true
- ai_spam_detection_model_allowed_seeded_models:
- default: ""
- hidden: true
- type: list
-
- ai_rag_images_enabled:
- default: false
- hidden: true
-
- ai_bot_enable_dedicated_ux:
- default: true
- client: true
-
- ai_translation_enabled:
- default: false
- client: true
- validator: "DiscourseAi::Configuration::LlmDependencyValidator"
- area: "ai-features/translation"
- ai_translation_model:
- default: ""
- type: enum
- allow_any: false
- enum: "DiscourseAi::Configuration::LlmEnumerator"
- validator: "DiscourseAi::Configuration::LlmValidator"
- area: "ai-features/translation"
- ai_translation_locale_detector_persona:
- default: "-27"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/translation"
- ai_translation_post_raw_translator_persona:
- default: "-28"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/translation"
- ai_translation_topic_title_translator_persona:
- default: "-29"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/translation"
- ai_translation_short_text_translator_persona:
- default: "-30"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/translation"
- ai_translation_backfill_hourly_rate:
- default: 0
- min: 0
- max: 1000
- client: false
- hidden: true
- area: "ai-features/translation"
- ai_translation_backfill_limit_to_public_content:
- default: true
- client: false
- area: "ai-features/translation"
- ai_translation_max_post_length:
- default: 10000
- client: false
- area: "ai-features/translation"
- ai_translation_backfill_max_age_days:
- default: 5
- min: 0
- max: 20000
- client: false
- area: "ai-features/translation"
- ai_translation_verbose_logs:
- default: false
- client: false
- hidden: true
- area: "ai-features/translation"
-
- inferred_concepts_enabled:
- default: false
- client: true
- area: "ai-features/inference"
- inferred_concepts_background_match:
- default: false
- client: false
- area: "ai-features/inference"
- inferred_concepts_daily_topics_limit:
- default: 20
- client: false
- area: "ai-features/inference"
- inferred_concepts_min_posts:
- default: 5
- client: false
- area: "ai-features/inference"
- inferred_concepts_min_likes:
- default: 10
- client: false
- area: "ai-features/inference"
- inferred_concepts_min_views:
- default: 100
- client: false
- area: "ai-features/inference"
- inferred_concepts_lookback_days:
- default: 30
- client: false
- area: "ai-features/inference"
- inferred_concepts_daily_posts_limit:
- default: 30
- client: false
- area: "ai-features/inference"
- inferred_concepts_post_min_likes:
- default: 5
- client: false
- area: "ai-features/inference"
- inferred_concepts_generate_persona:
- default: "-15"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/inference"
- inferred_concepts_match_persona:
- default: "-16"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/inference"
- inferred_concepts_deduplicate_persona:
- default: "-17"
- type: enum
- enum: "DiscourseAi::Configuration::PersonaEnumerator"
- area: "ai-features/inference"
- ai_artifact_kv_value_max_length:
- default: 5000
- hidden: true
- ai_artifact_max_keys_per_user_per_artifact:
- default: 100
- hidden: true
diff --git a/db/fixtures/ai_bot/602_bot_users.rb b/db/fixtures/ai_bot/602_bot_users.rb
deleted file mode 100644
index ae4d43f2..00000000
--- a/db/fixtures/ai_bot/602_bot_users.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-
-DiscourseAi::AiBot::SiteSettingsExtension.enable_or_disable_ai_bots
diff --git a/db/fixtures/personas/603_ai_personas.rb b/db/fixtures/personas/603_ai_personas.rb
deleted file mode 100644
index 9a351b0c..00000000
--- a/db/fixtures/personas/603_ai_personas.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-summarization_personas = [DiscourseAi::Personas::Summarizer, DiscourseAi::Personas::ShortSummarizer]
-
-def from_setting(setting_name)
- DB
- .query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: setting_name,
- )
- &.first
- &.split("|")
-end
-
-DiscourseAi::Personas::Persona.system_personas.each do |persona_class, id|
- persona = AiPersona.find_by(id: id)
- if !persona
- persona = AiPersona.new
- persona.id = id
-
- if persona_class == DiscourseAi::Personas::WebArtifactCreator
- # this is somewhat sensitive, so we default it to staff
- persona.allowed_group_ids = [Group::AUTO_GROUPS[:staff]]
- elsif summarization_personas.include?(persona_class)
- # Copy group permissions from site settings.
- default_groups = [Group::AUTO_GROUPS[:staff], Group::AUTO_GROUPS[:trust_level_3]]
-
- setting_name = "ai_custom_summarization_allowed_groups"
- if persona_class == DiscourseAi::Personas::ShortSummarizer
- setting_name = "ai_summary_gists_allowed_groups"
- default_groups = [Group::AUTO_GROUPS[:everyone]]
- end
-
- persona.allowed_group_ids = from_setting(setting_name) || default_groups
- elsif persona_class == DiscourseAi::Personas::CustomPrompt
- setting_name = "ai_helper_custom_prompts_allowed_groups"
- default_groups = [Group::AUTO_GROUPS[:staff]]
- persona.allowed_group_ids = from_setting(setting_name) || default_groups
- elsif persona_class == DiscourseAi::Personas::ContentCreator
- persona.allowed_group_ids = [Group::AUTO_GROUPS[:everyone]]
- else
- persona.allowed_group_ids = [Group::AUTO_GROUPS[:trust_level_0]]
- end
-
- persona.enabled = persona_class.default_enabled
- persona.priority = true if persona_class == DiscourseAi::Personas::General
- end
-
- names = [
- persona_class.name,
- persona_class.name + " 1",
- persona_class.name + " 2",
- persona_class.name + SecureRandom.hex,
- ]
- persona.name = DB.query_single(<<~SQL, names, id).first
- SELECT guess_name
- FROM (
- SELECT unnest(Array[?]) AS guess_name
- FROM (SELECT 1) as t
- ) x
- LEFT JOIN ai_personas ON ai_personas.name = x.guess_name AND ai_personas.id <> ?
- WHERE ai_personas.id IS NULL
- ORDER BY x.guess_name ASC
- LIMIT 1
- SQL
-
- persona.description = persona_class.description
-
- persona.system = true
- instance = persona_class.new
- tools = {}
- instance.tools.map { |tool| tool.to_s.split("::").last }.each { |name| tools[name] = nil }
- existing_tools = persona.tools || []
-
- existing_tools.each do |tool|
- if tool.is_a?(Array)
- name, value = tool
- tools[name] = value if tools.key?(name)
- end
- end
-
- persona.tools = tools.map { |name, value| [name, value] }
-
- persona.response_format = instance.response_format
- persona.examples = instance.examples
-
- persona.system_prompt = instance.system_prompt
- persona.top_p = instance.top_p
- persona.temperature = instance.temperature
- persona.save!(validate: false)
-end
diff --git a/db/migrate/20230224165056_create_classification_results_table.rb b/db/migrate/20230224165056_create_classification_results_table.rb
deleted file mode 100644
index 4b6e8a57..00000000
--- a/db/migrate/20230224165056_create_classification_results_table.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-class CreateClassificationResultsTable < ActiveRecord::Migration[7.0]
- def change
- create_table :classification_results do |t|
- t.string :model_used, null: true
- t.string :classification_type, null: true
- t.integer :target_id, null: true
- t.string :target_type, null: true
-
- t.jsonb :classification, null: true
- t.timestamps
- end
-
- add_index :classification_results,
- %i[target_id target_type model_used],
- unique: true,
- name: "unique_classification_target_per_type"
- end
-end
diff --git a/db/migrate/20230307125342_created_model_accuracy_table.rb b/db/migrate/20230307125342_created_model_accuracy_table.rb
deleted file mode 100644
index 3222bca5..00000000
--- a/db/migrate/20230307125342_created_model_accuracy_table.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-class CreatedModelAccuracyTable < ActiveRecord::Migration[7.0]
- def change
- create_table :model_accuracies do |t|
- t.string :model, null: false
- t.string :classification_type, null: false
- t.integer :flags_agreed, null: false, default: 0
- t.integer :flags_disagreed, null: false, default: 0
-
- t.timestamps
- end
-
- add_index :model_accuracies, %i[model], unique: true
- end
-end
diff --git a/db/migrate/20230314184514_migrate_discourse_ai_reviewables.rb b/db/migrate/20230314184514_migrate_discourse_ai_reviewables.rb
deleted file mode 100644
index 29d393e9..00000000
--- a/db/migrate/20230314184514_migrate_discourse_ai_reviewables.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-class MigrateDiscourseAiReviewables < ActiveRecord::Migration[7.0]
- def up
- DB.exec("UPDATE reviewables SET type='ReviewableAiPost' WHERE type='ReviewableAIPost'")
- DB.exec(
- "UPDATE reviewables SET type='ReviewableAiChatMessage' WHERE type='ReviewableAIChatMessage'",
- )
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20230316160714_create_completion_prompt_table.rb b/db/migrate/20230316160714_create_completion_prompt_table.rb
deleted file mode 100644
index a89c521d..00000000
--- a/db/migrate/20230316160714_create_completion_prompt_table.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-class CreateCompletionPromptTable < ActiveRecord::Migration[7.0]
- def change
- create_table :completion_prompts do |t|
- t.string :name, null: false
- t.string :translated_name
- t.integer :prompt_type, null: false, default: 0
- t.text :value, null: false
- t.boolean :enabled, null: false, default: true
- t.timestamps
- end
-
- add_index :completion_prompts, %i[name], unique: true
- end
-end
diff --git a/db/migrate/20230320122645_delete_duplicated_seeded_prompts.rb b/db/migrate/20230320122645_delete_duplicated_seeded_prompts.rb
deleted file mode 100644
index 81d06821..00000000
--- a/db/migrate/20230320122645_delete_duplicated_seeded_prompts.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-class DeleteDuplicatedSeededPrompts < ActiveRecord::Migration[7.0]
- def up
- DB.exec <<~SQL
- DELETE FROM completion_prompts
- WHERE (
- (id = 1 AND name = 'translate') OR
- (id = 2 AND name = 'generate_titles') OR
- (id = 3 AND name = 'proofread')
- )
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20230320185619_multi_message_completion_prompts.rb b/db/migrate/20230320185619_multi_message_completion_prompts.rb
deleted file mode 100644
index 64f1bac4..00000000
--- a/db/migrate/20230320185619_multi_message_completion_prompts.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class MultiMessageCompletionPrompts < ActiveRecord::Migration[7.0]
- def change
- add_column :completion_prompts, :messages, :jsonb
- end
-end
diff --git a/db/migrate/20230320191928_drop_completion_prompt_value.rb b/db/migrate/20230320191928_drop_completion_prompt_value.rb
deleted file mode 100644
index a7864fd0..00000000
--- a/db/migrate/20230320191928_drop_completion_prompt_value.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class DropCompletionPromptValue < ActiveRecord::Migration[7.0]
- def change
- remove_column :completion_prompts, :value, :text
- end
-end
diff --git a/db/migrate/20230322142028_make_dropped_value_column_nullable.rb b/db/migrate/20230322142028_make_dropped_value_column_nullable.rb
deleted file mode 100644
index 988e618d..00000000
--- a/db/migrate/20230322142028_make_dropped_value_column_nullable.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-class MakeDroppedValueColumnNullable < ActiveRecord::Migration[7.0]
- def up
- if column_exists?(:completion_prompts, :value)
- Migration::SafeMigrate.disable!
- change_column_null :completion_prompts, :value, true
- Migration::SafeMigrate.enable!
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20230406135943_add_provider_to_completion_prompts.rb b/db/migrate/20230406135943_add_provider_to_completion_prompts.rb
deleted file mode 100644
index 516419a7..00000000
--- a/db/migrate/20230406135943_add_provider_to_completion_prompts.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-class AddProviderToCompletionPrompts < ActiveRecord::Migration[7.0]
- def up
- remove_index :completion_prompts, name: "index_completion_prompts_on_name"
- add_column :completion_prompts, :provider, :text
- add_index :completion_prompts, %i[name], unique: false
-
- # set provider for existing prompts
- DB.exec <<~SQL
- UPDATE completion_prompts
- SET provider = 'openai'
- WHERE provider IS NULL;
- SQL
- end
-
- def down
- remove_column :completion_prompts, :provider
- remove_index :completion_prompts, name: "index_completion_prompts_on_name"
- add_index :completion_prompts, %i[name], unique: true
- end
-end
diff --git a/db/migrate/20230424055354_create_ai_api_audit_logs.rb b/db/migrate/20230424055354_create_ai_api_audit_logs.rb
deleted file mode 100644
index eb4c6413..00000000
--- a/db/migrate/20230424055354_create_ai_api_audit_logs.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class CreateAiApiAuditLogs < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_api_audit_logs do |t|
- t.integer :provider_id, null: false
- t.integer :user_id
- t.integer :request_tokens
- t.integer :response_tokens
- t.string :raw_request_payload
- t.string :raw_response_payload
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20230519003106_post_custom_prompts.rb b/db/migrate/20230519003106_post_custom_prompts.rb
deleted file mode 100644
index 7d72cb0d..00000000
--- a/db/migrate/20230519003106_post_custom_prompts.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-class PostCustomPrompts < ActiveRecord::Migration[7.0]
- def change
- create_table :post_custom_prompts do |t|
- t.integer :post_id, null: false
- t.json :custom_prompt, null: false
- t.timestamps
- end
-
- add_index :post_custom_prompts, :post_id, unique: true
- end
-end
diff --git a/db/migrate/20230710171141_enable_pg_vector_extension.rb b/db/migrate/20230710171141_enable_pg_vector_extension.rb
deleted file mode 100644
index a6acb319..00000000
--- a/db/migrate/20230710171141_enable_pg_vector_extension.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-class EnablePgVectorExtension < ActiveRecord::Migration[7.0]
- def change
- begin
- enable_extension :vector
- rescue Exception => e
- if DB.query_single("SELECT 1 FROM pg_available_extensions WHERE name = 'vector';").empty?
- STDERR.puts "------------------------------DISCOURSE AI ERROR----------------------------------"
- STDERR.puts " Discourse AI requires the pgvector extension on the PostgreSQL database."
- STDERR.puts " Run a `./launcher rebuild app` to fix it on a standard install."
- STDERR.puts " Alternatively, you can remove Discourse AI to rebuild."
- STDERR.puts "------------------------------DISCOURSE AI ERROR----------------------------------"
- end
- raise e
- end
- end
-end
diff --git a/db/migrate/20230710171142_create_ai_topic_embeddings_table.rb b/db/migrate/20230710171142_create_ai_topic_embeddings_table.rb
deleted file mode 100644
index 22756c1f..00000000
--- a/db/migrate/20230710171142_create_ai_topic_embeddings_table.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-class CreateAiTopicEmbeddingsTable < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_topic_embeddings_1_1, id: false do |t|
- t.integer :topic_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(768)", null: false
- t.timestamps
-
- t.index :topic_id, unique: true
- end
-
- create_table :ai_topic_embeddings_2_1, id: false do |t|
- t.integer :topic_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1536)", null: false
- t.timestamps
-
- t.index :topic_id, unique: true
- end
- end
-end
diff --git a/db/migrate/20230710171143_migrate_embeddings_from_dedicated_database.rb b/db/migrate/20230710171143_migrate_embeddings_from_dedicated_database.rb
deleted file mode 100644
index 3a60d99c..00000000
--- a/db/migrate/20230710171143_migrate_embeddings_from_dedicated_database.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-# frozen_string_literal: true
-
-class MigrateEmbeddingsFromDedicatedDatabase < ActiveRecord::Migration[7.0]
- def up
- return unless SiteSetting.ai_embeddings_enabled
- return if SiteSetting.ai_embeddings_pg_connection_string.blank?
-
- truncation = DiscourseAi::Embeddings::Strategies::Truncation.new
-
- vector_reps =
- [
- DiscourseAi::Embeddings::VectorRepresentations::AllMpnetBaseV2,
- DiscourseAi::Embeddings::VectorRepresentations::TextEmbeddingAda002,
- ].map { |k| k.new(truncation) }
-
- vector_reps.each do |vector_rep|
- new_table_name = DiscourseAi::Embeddings::Schema::TOPICS_TABLE
- old_table_name = "topic_embeddings_#{vector_rep.name.underscore}"
-
- begin
- row_count =
- DiscourseAi::Database::Connection
- .db
- .query_single("SELECT COUNT(*) FROM #{old_table_name}")
- .first
-
- if row_count > 0
- puts "Migrating #{row_count} embeddings from #{old_table_name} to #{new_table_name}"
-
- last_topic_id = 0
-
- loop do
- batch = DiscourseAi::Database::Connection.db.query(<<-SQL)
- SELECT topic_id, embedding
- FROM #{old_table_name}
- WHERE topic_id > #{last_topic_id}
- ORDER BY topic_id ASC
- LIMIT 50
- SQL
- break if batch.empty?
-
- DB.exec(<<-SQL)
- INSERT INTO #{new_table_name} (topic_id, model_version, strategy_version, digest, embeddings, created_at, updated_at)
- VALUES #{batch.map { |r| "(#{r.topic_id}, 0, 0, '', '#{r.embedding}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" }.join(", ")}
- ON CONFLICT (topic_id)
- DO NOTHING
- SQL
-
- last_topic_id = batch.last.topic_id
- end
- end
- rescue PG::Error => e
- Rails.logger.error(
- "Error #{e} migrating embeddings from #{old_table_name} to #{new_table_name}",
- )
- end
- end
- end
-
- def down
- # no-op
- end
-end
diff --git a/db/migrate/20230727170222_create_multilingual_topic_embeddings_table.rb b/db/migrate/20230727170222_create_multilingual_topic_embeddings_table.rb
deleted file mode 100644
index 4da6b5c4..00000000
--- a/db/migrate/20230727170222_create_multilingual_topic_embeddings_table.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-class CreateMultilingualTopicEmbeddingsTable < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_topic_embeddings_3_1, id: false do |t|
- t.integer :topic_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1024)", null: false
- t.timestamps
-
- t.index :topic_id, unique: true
- end
- end
-end
diff --git a/db/migrate/20230831033812_rename_ai_helper_add_ai_pm_to_header_setting.rb b/db/migrate/20230831033812_rename_ai_helper_add_ai_pm_to_header_setting.rb
deleted file mode 100644
index 1db79970..00000000
--- a/db/migrate/20230831033812_rename_ai_helper_add_ai_pm_to_header_setting.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class RenameAiHelperAddAiPmToHeaderSetting < ActiveRecord::Migration[7.0]
- def up
- execute "UPDATE site_settings SET name = 'ai_bot_add_to_header' WHERE name = 'ai_helper_add_ai_pm_to_header'"
- end
-
- def down
- execute "UPDATE site_settings SET name = 'ai_helper_add_ai_pm_to_header' WHERE name = 'ai_bot_add_to_header'"
- end
-end
diff --git a/db/migrate/20231003155701_create_bge_topic_embeddings_table.rb b/db/migrate/20231003155701_create_bge_topic_embeddings_table.rb
deleted file mode 100644
index e83e47e2..00000000
--- a/db/migrate/20231003155701_create_bge_topic_embeddings_table.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-class CreateBgeTopicEmbeddingsTable < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_topic_embeddings_4_1, id: false do |t|
- t.integer :topic_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1024)", null: false
- t.timestamps
-
- t.index :topic_id, unique: true
- end
- end
-end
diff --git a/db/migrate/20231031050538_add_topic_id_post_id_to_ai_audit_log.rb b/db/migrate/20231031050538_add_topic_id_post_id_to_ai_audit_log.rb
deleted file mode 100644
index 9f91cf59..00000000
--- a/db/migrate/20231031050538_add_topic_id_post_id_to_ai_audit_log.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-class AddTopicIdPostIdToAiAuditLog < ActiveRecord::Migration[7.0]
- def change
- add_column :ai_api_audit_logs, :topic_id, :integer
- add_column :ai_api_audit_logs, :post_id, :integer
- end
-end
diff --git a/db/migrate/20231109011155_create_ai_personas.rb b/db/migrate/20231109011155_create_ai_personas.rb
deleted file mode 100644
index da89d3ab..00000000
--- a/db/migrate/20231109011155_create_ai_personas.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-#
-class CreateAiPersonas < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_personas do |t|
- t.string :name, null: false, unique: true, limit: 100
- t.string :description, null: false, limit: 2000
- t.string :commands, array: true, default: [], null: false
- t.string :system_prompt, null: false, limit: 10_000_000
- t.integer :allowed_group_ids, array: true, default: [], null: false
- t.integer :created_by_id
- t.boolean :enabled, default: true, null: false
- t.timestamps
- end
-
- add_index :ai_personas, :name, unique: true
- end
-end
diff --git a/db/migrate/20231117050928_add_system_and_priority_to_ai_personas.rb b/db/migrate/20231117050928_add_system_and_priority_to_ai_personas.rb
deleted file mode 100644
index 5c848ab6..00000000
--- a/db/migrate/20231117050928_add_system_and_priority_to_ai_personas.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-class AddSystemAndPriorityToAiPersonas < ActiveRecord::Migration[7.0]
- def change
- add_column :ai_personas, :system, :boolean, null: false, default: false
- add_column :ai_personas, :priority, :boolean, null: false, default: false
- end
-end
diff --git a/db/migrate/20231120033747_remove_site_settings.rb b/db/migrate/20231120033747_remove_site_settings.rb
deleted file mode 100644
index ea01a656..00000000
--- a/db/migrate/20231120033747_remove_site_settings.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-class RemoveSiteSettings < ActiveRecord::Migration[7.0]
- def up
- DB.exec(<<~SQL, %w[ai_bot_enabled_chat_commands ai_bot_enabled_personas])
- DELETE FROM site_settings WHERE name IN (?)
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20231123224203_switch_to_generic_completion_prompts.rb b/db/migrate/20231123224203_switch_to_generic_completion_prompts.rb
deleted file mode 100644
index 14dd61c1..00000000
--- a/db/migrate/20231123224203_switch_to_generic_completion_prompts.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class SwitchToGenericCompletionPrompts < ActiveRecord::Migration[7.0]
- def change
- remove_column :completion_prompts, :provider, :text
-
- DB.exec("DELETE FROM completion_prompts WHERE (id < 0 AND id > -300)")
- end
-end
diff --git a/db/migrate/20231128151234_recreate_generate_titles_prompt.rb b/db/migrate/20231128151234_recreate_generate_titles_prompt.rb
deleted file mode 100644
index 7153295a..00000000
--- a/db/migrate/20231128151234_recreate_generate_titles_prompt.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class RecreateGenerateTitlesPrompt < ActiveRecord::Migration[7.0]
- def up
- DB.exec("DELETE FROM completion_prompts WHERE id = -302")
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20231202013850_convert_ai_personas_commands_to_json.rb b/db/migrate/20231202013850_convert_ai_personas_commands_to_json.rb
deleted file mode 100644
index 6894dd57..00000000
--- a/db/migrate/20231202013850_convert_ai_personas_commands_to_json.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-class ConvertAiPersonasCommandsToJson < ActiveRecord::Migration[7.0]
- def up
- # this all may be a bit surprising, but interestingly this makes all our backend code
- # cross compatible
- # upgrading ["a", "b", "c"] to json simply works cause in both cases
- # rails will cast to a string array and all code simply expects a string array
- #
- # this change was made so we can also start storing parameters with the commands
- execute <<~SQL
- ALTER TABLE ai_personas
- ALTER COLUMN commands DROP DEFAULT
- SQL
-
- execute <<~SQL
- ALTER TABLE ai_personas
- ALTER COLUMN commands
- TYPE json USING array_to_json(commands)
- SQL
-
- execute <<~SQL
- ALTER TABLE ai_personas
- ALTER COLUMN commands
- SET DEFAULT '[]'::json
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20231227223301_create_gemini_topic_embeddings_table.rb b/db/migrate/20231227223301_create_gemini_topic_embeddings_table.rb
deleted file mode 100644
index 04a532c1..00000000
--- a/db/migrate/20231227223301_create_gemini_topic_embeddings_table.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-class CreateGeminiTopicEmbeddingsTable < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_topic_embeddings_5_1, id: false do |t|
- t.integer :topic_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(768)", null: false
- t.timestamps
-
- t.index :topic_id, unique: true
- end
- end
-end
diff --git a/db/migrate/20231228213036_create_ai_post_embeddings_tables.rb b/db/migrate/20231228213036_create_ai_post_embeddings_tables.rb
deleted file mode 100644
index f6ad3174..00000000
--- a/db/migrate/20231228213036_create_ai_post_embeddings_tables.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-# frozen_string_literal: true
-
-class CreateAiPostEmbeddingsTables < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_post_embeddings_1_1, id: false do |t|
- t.integer :post_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(768)", null: false
- t.timestamps
-
- t.index :post_id, unique: true
- end
-
- create_table :ai_post_embeddings_2_1, id: false do |t|
- t.integer :post_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1536)", null: false
- t.timestamps
-
- t.index :post_id, unique: true
- end
-
- create_table :ai_post_embeddings_3_1, id: false do |t|
- t.integer :post_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1024)", null: false
- t.timestamps
-
- t.index :post_id, unique: true
- end
-
- create_table :ai_post_embeddings_4_1, id: false do |t|
- t.integer :post_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1024)", null: false
- t.timestamps
-
- t.index :post_id, unique: true
- end
-
- create_table :ai_post_embeddings_5_1, id: false do |t|
- t.integer :post_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(768)", null: false
- t.timestamps
-
- t.index :post_id, unique: true
- end
- end
-end
diff --git a/db/migrate/20240104013944_add_params_to_completion_prompt.rb b/db/migrate/20240104013944_add_params_to_completion_prompt.rb
deleted file mode 100644
index 7d179e13..00000000
--- a/db/migrate/20240104013944_add_params_to_completion_prompt.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-class AddParamsToCompletionPrompt < ActiveRecord::Migration[7.0]
- def change
- add_column :completion_prompts, :temperature, :integer
- add_column :completion_prompts, :stop_sequences, :string, array: true
- end
-end
diff --git a/db/migrate/20240119152348_explicit_provider_backwards_compat.rb b/db/migrate/20240119152348_explicit_provider_backwards_compat.rb
deleted file mode 100644
index 86ee7f16..00000000
--- a/db/migrate/20240119152348_explicit_provider_backwards_compat.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-class ExplicitProviderBackwardsCompat < ActiveRecord::Migration[7.0]
- def up
- backfill_settings("composer_ai_helper_enabled", "ai_helper_model")
- backfill_settings(
- "ai_embeddings_semantic_search_enabled",
- "ai_embeddings_semantic_search_hyde_model",
- )
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-
- def backfill_settings(feature_setting_name, llm_setting_name)
- feature_enabled =
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: feature_setting_name,
- ).first == "t"
-
- setting_value =
- DB
- .query_single(
- "SELECT value FROM site_settings WHERE name = :llm_setting",
- llm_setting: llm_setting_name,
- )
- .first
- .to_s
-
- providers = %w[aws_bedrock anthropic open_ai hugging_face vllm google]
- # Sanity check to make sure we won't add provider twice.
- return if providers.include?(setting_value.split(":").first)
-
- if !setting_value && feature_enabled
- # Enabled and using old default (gpt-3.5-turbo)
- DB.exec(
- "UPDATE site_settings SET value='open_ai:gpt-3.5-turbo' WHERE name=:llm_setting",
- llm_setting: llm_setting_name,
- )
- elsif setting_value && !feature_enabled
- # They'll have to choose an LLM model again before enabling the feature
- DB.exec("DELETE FROM site_settings WHERE name=:llm_setting", llm_setting: llm_setting_name)
- elsif setting_value && feature_enabled
- DB.exec(
- "UPDATE site_settings SET value=:new_value WHERE name=:llm_setting",
- llm_setting: llm_setting_name,
- new_value: append_provider(setting_value),
- )
- end
- end
-
- def append_provider(value)
- open_ai_models = %w[gpt-3.5-turbo gpt-4 gpt-3.5-turbo-16k gpt-4-32k gpt-4-turbo gpt-4o]
- return "open_ai:#{value}" if open_ai_models.include?(value)
- return "google:#{value}" if value == "gemini-pro"
-
- hf_models = %w[StableBeluga2 Upstage-Llama-2-*-instruct-v2 Llama2-*-chat-hf Llama2-chat-hf]
- return "hugging_face:#{value}" if hf_models.include?(value)
-
- # Models available through multiple providers
- claude_models = %w[claude-instant-1 claude-2]
- if claude_models.include?(value)
- has_bedrock_creds =
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = 'ai_bedrock_secret_access_key' OR name = 'ai_bedrock_access_key_id' ",
- ).length > 0
-
- if has_bedrock_creds
- return "aws_bedrock:#{value}"
- else
- return "anthropic:#{value}"
- end
- end
-
- mixtral_models = %w[mistralai/Mixtral-8x7B-Instruct-v0.1 mistralai/Mistral-7B-Instruct-v0.2]
- if mixtral_models.include?(value)
- vllm_configured =
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = 'ai_vllm_endpoint_srv' OR name = 'ai_vllm_endpoint' ",
- ).length > 0
-
- if vllm_configured
- "vllm:#{value}"
- else
- "hugging_face:#{value}"
- end
- end
- end
-end
diff --git a/db/migrate/20240126013358_create_openai_text_embedding_tables.rb b/db/migrate/20240126013358_create_openai_text_embedding_tables.rb
deleted file mode 100644
index 6758ba17..00000000
--- a/db/migrate/20240126013358_create_openai_text_embedding_tables.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-class CreateOpenaiTextEmbeddingTables < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_topic_embeddings_6_1, id: false do |t|
- t.integer :topic_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1536)", null: false
- t.timestamps
-
- t.index :topic_id, unique: true
- end
-
- create_table :ai_topic_embeddings_7_1, id: false do |t|
- t.integer :topic_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(2000)", null: false
- t.timestamps
-
- t.index :topic_id, unique: true
- end
-
- create_table :ai_post_embeddings_6_1, id: false do |t|
- t.integer :post_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1536)", null: false
- t.timestamps
-
- t.index :post_id, unique: true
- end
-
- create_table :ai_post_embeddings_7_1, id: false do |t|
- t.integer :post_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(2000)", null: false
- t.timestamps
-
- t.index :post_id, unique: true
- end
- end
-end
diff --git a/db/migrate/20240202010752_add_temperature_top_p_to_ai_personas.rb b/db/migrate/20240202010752_add_temperature_top_p_to_ai_personas.rb
deleted file mode 100644
index a101338f..00000000
--- a/db/migrate/20240202010752_add_temperature_top_p_to_ai_personas.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-class AddTemperatureTopPToAiPersonas < ActiveRecord::Migration[7.0]
- def change
- add_column :ai_personas, :temperature, :float, null: true
- add_column :ai_personas, :top_p, :float, null: true
- end
-end
diff --git a/db/migrate/20240207144910_fix_llm_backed_setting_defaults.rb b/db/migrate/20240207144910_fix_llm_backed_setting_defaults.rb
deleted file mode 100644
index 105ee15a..00000000
--- a/db/migrate/20240207144910_fix_llm_backed_setting_defaults.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-# Some sites defaults weren't migrated correctly due to the previous migration
-# using the value as the if condition instead of checking with String#empty?
-class FixLlmBackedSettingDefaults < ActiveRecord::Migration[7.0]
- def up
- backfill_settings("composer_ai_helper_enabled", "ai_helper_model")
- backfill_settings(
- "ai_embeddings_semantic_search_enabled",
- "ai_embeddings_semantic_search_hyde_model",
- )
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-
- def backfill_settings(feature_setting_name, llm_setting_name)
- feature_enabled =
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: feature_setting_name,
- ).first == "t"
-
- setting_value =
- DB
- .query_single(
- "SELECT value FROM site_settings WHERE name = :llm_setting",
- llm_setting: llm_setting_name,
- )
- .first
- .to_s
- using_old_default = setting_value.empty?
-
- providers = %w[aws_bedrock anthropic open_ai hugging_face vllm google]
- # Sanity check to make sure we won't add provider twice.
- return if providers.include?(setting_value.split(":").first)
-
- if using_old_default && feature_enabled
- # Enabled and using old default (gpt-3.5-turbo)
- DB.exec(<<~SQL, llm_setting: llm_setting_name, default: "open_ai:gpt-3.5-turbo")
- INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
- VALUES (:llm_setting, 1, :default, NOW(), NOW())
- SQL
- end
- end
-end
diff --git a/db/migrate/20240209044519_add_user_id_mentionable_default_llm_to_ai_personas.rb b/db/migrate/20240209044519_add_user_id_mentionable_default_llm_to_ai_personas.rb
deleted file mode 100644
index cf5e0315..00000000
--- a/db/migrate/20240209044519_add_user_id_mentionable_default_llm_to_ai_personas.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-#
-class AddUserIdMentionableDefaultLlmToAiPersonas < ActiveRecord::Migration[7.0]
- def change
- change_table :ai_personas do |t|
- t.integer :user_id, null: true
- t.boolean :mentionable, default: false, null: false
- t.text :default_llm, null: true, length: 250
- end
- end
-end
diff --git a/db/migrate/20240213051213_add_limits_to_ai_persona.rb b/db/migrate/20240213051213_add_limits_to_ai_persona.rb
deleted file mode 100644
index fac906e4..00000000
--- a/db/migrate/20240213051213_add_limits_to_ai_persona.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class AddLimitsToAiPersona < ActiveRecord::Migration[7.0]
- def change
- change_table :ai_personas do |t|
- t.integer :max_context_posts, null: true
- end
- end
-end
diff --git a/db/migrate/20240309034751_add_shared_ai_conversations.rb b/db/migrate/20240309034751_add_shared_ai_conversations.rb
deleted file mode 100644
index 37e910c7..00000000
--- a/db/migrate/20240309034751_add_shared_ai_conversations.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-class AddSharedAiConversations < ActiveRecord::Migration[7.0]
- def up
- create_table :shared_ai_conversations do |t|
- t.integer :user_id, null: false
- t.integer :target_id, null: false
- t.string :target_type, null: false, max_length: 100
- t.string :title, null: false, max_length: 1024
- t.string :llm_name, null: false, max_length: 1024
- t.jsonb :context, null: false
- t.string :share_key, null: false, index: { unique: true }
- t.string :excerpt, null: false, max_length: 10_000
- t.timestamps
- end
-
- add_index :shared_ai_conversations, %i[target_id target_type], unique: true
- add_index :shared_ai_conversations,
- %i[user_id target_id target_type],
- unique: true,
- name: "idx_shared_ai_conversations_user_target"
- end
-
- def down
- drop_table :shared_ai_conversations
- end
-end
diff --git a/db/migrate/20240309034752_create_rag_document_fragment_table.rb b/db/migrate/20240309034752_create_rag_document_fragment_table.rb
deleted file mode 100644
index a6a3e233..00000000
--- a/db/migrate/20240309034752_create_rag_document_fragment_table.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-class CreateRagDocumentFragmentTable < ActiveRecord::Migration[7.0]
- def change
- create_table :rag_document_fragments do |t|
- t.text :fragment, null: false
- t.integer :upload_id, null: false
- t.integer :ai_persona_id, null: false
- t.integer :fragment_number, null: false
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20240313165121_embedding_tables_for_rag_uploads.rb b/db/migrate/20240313165121_embedding_tables_for_rag_uploads.rb
deleted file mode 100644
index 0aa7d502..00000000
--- a/db/migrate/20240313165121_embedding_tables_for_rag_uploads.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-# frozen_string_literal: true
-
-class EmbeddingTablesForRagUploads < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_document_fragment_embeddings_1_1, id: false do |t|
- t.integer :rag_document_fragment_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(768)", null: false
- t.timestamps
-
- t.index :rag_document_fragment_id,
- unique: true,
- name: "rag_document_fragment_id_embeddings_1_1"
- end
-
- create_table :ai_document_fragment_embeddings_2_1, id: false do |t|
- t.integer :rag_document_fragment_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1536)", null: false
- t.timestamps
-
- t.index :rag_document_fragment_id,
- unique: true,
- name: "rag_document_fragment_id_embeddings_2_1"
- end
-
- create_table :ai_document_fragment_embeddings_3_1, id: false do |t|
- t.integer :rag_document_fragment_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1024)", null: false
- t.timestamps
-
- t.index :rag_document_fragment_id,
- unique: true,
- name: "rag_document_fragment_id_embeddings_3_1"
- end
-
- create_table :ai_document_fragment_embeddings_4_1, id: false do |t|
- t.integer :rag_document_fragment_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1024)", null: false
- t.timestamps
-
- t.index :rag_document_fragment_id,
- unique: true,
- name: "rag_document_fragment_id_embeddings_4_1"
- end
-
- create_table :ai_document_fragment_embeddings_5_1, id: false do |t|
- t.integer :rag_document_fragment_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(768)", null: false
- t.timestamps
-
- t.index :rag_document_fragment_id,
- unique: true,
- name: "rag_document_fragment_id_embeddings_5_1"
- end
-
- create_table :ai_document_fragment_embeddings_6_1, id: false do |t|
- t.integer :rag_document_fragment_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1536)", null: false
- t.timestamps
-
- t.index :rag_document_fragment_id,
- unique: true,
- name: "rag_document_fragment_id_embeddings_6_1"
- end
-
- create_table :ai_document_fragment_embeddings_7_1, id: false do |t|
- t.integer :rag_document_fragment_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(2000)", null: false
- t.timestamps
-
- t.index :rag_document_fragment_id,
- unique: true,
- name: "rag_document_fragment_id_embeddings_7_1"
- end
- end
-end
diff --git a/db/migrate/20240322035907_add_images_to_ai_personas.rb b/db/migrate/20240322035907_add_images_to_ai_personas.rb
deleted file mode 100644
index b78b1c33..00000000
--- a/db/migrate/20240322035907_add_images_to_ai_personas.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-class AddImagesToAiPersonas < ActiveRecord::Migration[7.0]
- def change
- change_table :ai_personas do |t|
- add_column :ai_personas, :vision_enabled, :boolean, default: false, null: false
- add_column :ai_personas, :vision_max_pixels, :integer, default: 1_048_576, null: false
- end
- end
-end
diff --git a/db/migrate/20240404000838_add_metadata_to_rag_document_frament.rb b/db/migrate/20240404000838_add_metadata_to_rag_document_frament.rb
deleted file mode 100644
index 9614979d..00000000
--- a/db/migrate/20240404000838_add_metadata_to_rag_document_frament.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-class AddMetadataToRagDocumentFrament < ActiveRecord::Migration[7.0]
- def change
- # limit is purely for safety
- add_column :rag_document_fragments, :metadata, :text, null: true, limit: 100_000
- end
-end
diff --git a/db/migrate/20240409035951_add_rag_params_to_ai_persona.rb b/db/migrate/20240409035951_add_rag_params_to_ai_persona.rb
deleted file mode 100644
index 2cb2d5b2..00000000
--- a/db/migrate/20240409035951_add_rag_params_to_ai_persona.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class AddRagParamsToAiPersona < ActiveRecord::Migration[7.0]
- def change
- # the default fits without any data loss in a 384 token vector representation
- # larger embedding models can easily fit larger chunks so this is configurable
- add_column :ai_personas, :rag_chunk_tokens, :integer, null: false, default: 374
- add_column :ai_personas, :rag_chunk_overlap_tokens, :integer, null: false, default: 10
- add_column :ai_personas, :rag_conversation_chunks, :integer, null: false, default: 10
- end
-end
diff --git a/db/migrate/20240410170000_add_embeddings_tablesfor_bge_m3.rb b/db/migrate/20240410170000_add_embeddings_tablesfor_bge_m3.rb
deleted file mode 100644
index 95ecd0c5..00000000
--- a/db/migrate/20240410170000_add_embeddings_tablesfor_bge_m3.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-class AddEmbeddingsTablesforBgeM3 < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_topic_embeddings_8_1, id: false do |t|
- t.integer :topic_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1024)", null: false
- t.timestamps
-
- t.index :topic_id, unique: true
- end
- create_table :ai_post_embeddings_8_1, id: false do |t|
- t.integer :post_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1024)", null: false
- t.timestamps
-
- t.index :post_id, unique: true
- end
- create_table :ai_document_fragment_embeddings_8_1, id: false do |t|
- t.integer :rag_document_fragment_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "vector(1024)", null: false
- t.timestamps
-
- t.index :rag_document_fragment_id,
- unique: true,
- name: "rag_document_fragment_id_embeddings_8_1"
- end
- end
-end
diff --git a/db/migrate/20240424220101_add_auto_image_caption_to_user_options.rb b/db/migrate/20240424220101_add_auto_image_caption_to_user_options.rb
deleted file mode 100644
index 329d2204..00000000
--- a/db/migrate/20240424220101_add_auto_image_caption_to_user_options.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class AddAutoImageCaptionToUserOptions < ActiveRecord::Migration[7.0]
- def change
- add_column :user_options, :auto_image_caption, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20240429065155_add_consolidated_question_llm_to_ai_persona.rb b/db/migrate/20240429065155_add_consolidated_question_llm_to_ai_persona.rb
deleted file mode 100644
index 963dffad..00000000
--- a/db/migrate/20240429065155_add_consolidated_question_llm_to_ai_persona.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class AddConsolidatedQuestionLlmToAiPersona < ActiveRecord::Migration[7.0]
- def change
- add_column :ai_personas, :question_consolidator_llm, :text, max_length: 2000
- end
-end
diff --git a/db/migrate/20240503034946_add_allow_chat_to_ai_persona.rb b/db/migrate/20240503034946_add_allow_chat_to_ai_persona.rb
deleted file mode 100644
index cfba263b..00000000
--- a/db/migrate/20240503034946_add_allow_chat_to_ai_persona.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class AddAllowChatToAiPersona < ActiveRecord::Migration[7.0]
- def change
- add_column :ai_personas, :allow_chat, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20240503042558_add_chat_message_custom_prompt.rb b/db/migrate/20240503042558_add_chat_message_custom_prompt.rb
deleted file mode 100644
index aceb7c79..00000000
--- a/db/migrate/20240503042558_add_chat_message_custom_prompt.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-class AddChatMessageCustomPrompt < ActiveRecord::Migration[7.0]
- def change
- create_table :chat_message_custom_prompts do |t|
- t.bigint :message_id, null: false
- t.json :custom_prompt, null: false
- t.timestamps
- end
-
- add_index :chat_message_custom_prompts, :message_id, unique: true
- end
-end
diff --git a/db/migrate/20240504222307_create_llm_model_table.rb b/db/migrate/20240504222307_create_llm_model_table.rb
deleted file mode 100644
index 96bcc3dd..00000000
--- a/db/migrate/20240504222307_create_llm_model_table.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-class CreateLlmModelTable < ActiveRecord::Migration[7.0]
- def change
- create_table :llm_models do |t|
- t.string :display_name
- t.string :name, null: false
- t.string :provider, null: false
- t.string :tokenizer, null: false
- t.integer :max_prompt_tokens, null: false
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20240514001334_add_feature_name_to_ai_api_audit_log.rb b/db/migrate/20240514001334_add_feature_name_to_ai_api_audit_log.rb
deleted file mode 100644
index 1f1f08dc..00000000
--- a/db/migrate/20240514001334_add_feature_name_to_ai_api_audit_log.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class AddFeatureNameToAiApiAuditLog < ActiveRecord::Migration[7.0]
- def change
- add_column :ai_api_audit_logs, :feature_name, :string, limit: 255
- end
-end
diff --git a/db/migrate/20240514171609_add_endpoint_data_to_llm_model.rb b/db/migrate/20240514171609_add_endpoint_data_to_llm_model.rb
deleted file mode 100644
index 7be5e9d0..00000000
--- a/db/migrate/20240514171609_add_endpoint_data_to_llm_model.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-class AddEndpointDataToLlmModel < ActiveRecord::Migration[7.0]
- def change
- add_column :llm_models, :url, :string
- add_column :llm_models, :api_key, :string
- end
-end
diff --git a/db/migrate/20240527054218_add_language_model_to_ai_audit_logs.rb b/db/migrate/20240527054218_add_language_model_to_ai_audit_logs.rb
deleted file mode 100644
index 5f639394..00000000
--- a/db/migrate/20240527054218_add_language_model_to_ai_audit_logs.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class AddLanguageModelToAiAuditLogs < ActiveRecord::Migration[7.0]
- def change
- add_column :ai_api_audit_logs, :language_model, :string, limit: 255
- end
-end
diff --git a/db/migrate/20240528132059_add_companion_user_to_llm_model.rb b/db/migrate/20240528132059_add_companion_user_to_llm_model.rb
deleted file mode 100644
index e512e338..00000000
--- a/db/migrate/20240528132059_add_companion_user_to_llm_model.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-class AddCompanionUserToLlmModel < ActiveRecord::Migration[7.0]
- def change
- add_column :llm_models, :user_id, :integer
- add_column :llm_models, :enabled_chat_bot, :boolean, null: false, default: false
- end
-end
diff --git a/db/migrate/20240528144216_seed_open_ai_models.rb b/db/migrate/20240528144216_seed_open_ai_models.rb
deleted file mode 100644
index 0d6fde0a..00000000
--- a/db/migrate/20240528144216_seed_open_ai_models.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-# frozen_string_literal: true
-
-class SeedOpenAiModels < ActiveRecord::Migration[7.0]
- def up
- models = []
-
- open_ai_api_key = fetch_setting("ai_openai_api_key")
- enabled_models = fetch_setting("ai_bot_enabled_chat_bots")&.split("|").to_a
- enabled_models = ["gpt-3.5-turbo-16k"] if enabled_models.empty?
-
- if open_ai_api_key.present?
- models << mirror_open_ai(
- "GPT-3.5-Turbo",
- "gpt-3.5-turbo",
- 8192,
- "ai_openai_gpt35_url",
- open_ai_api_key,
- -111,
- enabled_models,
- )
- models << mirror_open_ai(
- "GPT-3.5-Turbo-16K",
- "gpt-3.5-turbo-16k",
- 16_384,
- "ai_openai_gpt35_16k_url",
- open_ai_api_key,
- -111,
- enabled_models,
- )
- models << mirror_open_ai(
- "GPT-4",
- "gpt-4",
- 8192,
- "ai_openai_gpt4_url",
- open_ai_api_key,
- -110,
- enabled_models,
- )
- models << mirror_open_ai(
- "GPT-4-32K",
- "gpt-4-32k",
- 32_768,
- "ai_openai_gpt4_32k_url",
- open_ai_api_key,
- -110,
- enabled_models,
- )
- models << mirror_open_ai(
- "GPT-4-Turbo",
- "gpt-4-turbo",
- 131_072,
- "ai_openai_gpt4_turbo_url",
- open_ai_api_key,
- -113,
- enabled_models,
- )
- models << mirror_open_ai(
- "GPT-4o",
- "gpt-4o",
- 131_072,
- "ai_openai_gpt4o_url",
- open_ai_api_key,
- -121,
- enabled_models,
- )
- end
-
- if models.present?
- rows = models.compact.join(", ")
-
- DB.exec(<<~SQL) if rows.present?
- INSERT INTO llm_models(display_name, name, provider, tokenizer, max_prompt_tokens, url, api_key, user_id, enabled_chat_bot, created_at, updated_at)
- VALUES #{rows};
- SQL
- end
- end
-
- def has_companion_user?(user_id)
- DB.query_single("SELECT id FROM users WHERE id = :user_id", user_id: user_id).first.present?
- end
-
- def fetch_setting(name)
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: name,
- ).first
- end
-
- def mirror_open_ai(
- display_name,
- name,
- max_prompt_tokens,
- setting_name,
- key,
- bot_id,
- enabled_models
- )
- url = fetch_setting(setting_name) || "https://api.openai.com/v1/chat/completions"
- user_id = has_companion_user?(bot_id) ? bot_id : "NULL"
- enabled = enabled_models.include?(name)
-
- "('#{display_name}', '#{name}', 'open_ai', 'DiscourseAi::Tokenizer::OpenAiTokenizer', #{max_prompt_tokens}, '#{url}', '#{key}', #{user_id}, #{enabled}, NOW(), NOW())"
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240531205234_seed_claude_models.rb b/db/migrate/20240531205234_seed_claude_models.rb
deleted file mode 100644
index 4b6dad40..00000000
--- a/db/migrate/20240531205234_seed_claude_models.rb
+++ /dev/null
@@ -1,109 +0,0 @@
-# frozen_string_literal: true
-
-class SeedClaudeModels < ActiveRecord::Migration[7.0]
- def up
- claude_models = %w[claude-instant-1 claude-2 claude-3-haiku claude-3-sonnet claude-3-opus]
-
- models = []
-
- bedrock_secret_access_key = fetch_setting("ai_bedrock_secret_access_key")
- enabled_models = fetch_setting("ai_bot_enabled_chat_bots")&.split("|").to_a
-
- if bedrock_secret_access_key.present?
- bedrock_region = fetch_setting("ai_bedrock_region") || "us-east-1"
-
- claude_models.each do |cm|
- url =
- "https://bedrock-runtime.#{bedrock_region}.amazonaws.com/model/#{mapped_bedrock_model(cm)}/invoke"
-
- bot_id = claude_bot_id(cm)
- user_id = has_companion_user?(bot_id) ? bot_id : "NULL"
-
- enabled = enabled_models.include?(cm)
- models << "('#{display_name(cm)}', '#{cm}', 'aws_bedrock', 'DiscourseAi::Tokenizer::AnthropicTokenizer', 200000, '#{url}', '#{bedrock_secret_access_key}', #{user_id}, #{enabled}, NOW(), NOW())"
- end
- end
-
- anthropic_ai_api_key = fetch_setting("ai_anthropic_api_key")
- if anthropic_ai_api_key.present?
- claude_models.each do |cm|
- url = "https://api.anthropic.com/v1/messages"
-
- bot_id = claude_bot_id(cm)
- user_id = has_companion_user?(bot_id) ? bot_id : "NULL"
-
- enabled = enabled_models.include?(cm)
- models << "('#{display_name(cm)}', '#{cm}', 'anthropic', 'DiscourseAi::Tokenizer::AnthropicTokenizer', 200000, '#{url}', '#{anthropic_ai_api_key}', #{user_id}, #{enabled}, NOW(), NOW())"
- end
- end
-
- if models.present?
- rows = models.compact.join(", ")
-
- DB.exec(<<~SQL, rows: rows) if rows.present?
- INSERT INTO llm_models(display_name, name, provider, tokenizer, max_prompt_tokens, url, api_key, user_id, enabled_chat_bot, created_at, updated_at)
- VALUES #{rows};
- SQL
- end
- end
-
- def has_companion_user?(user_id)
- DB.query_single("SELECT id FROM users WHERE id = :user_id", user_id: user_id).first.present?
- end
-
- def fetch_setting(name)
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: name,
- ).first
- end
-
- def claude_bot_id(model)
- case model
- when "claude-2"
- -112
- when "claude-3-haiku"
- -119
- when "claude-3-sonnet"
- -118
- when "claude-instant-1"
- nil
- when "claude-3-opus"
- -117
- end
- end
-
- def mapped_bedrock_model(model)
- case model
- when "claude-2"
- "anthropic.claude-v2:1"
- when "claude-3-haiku"
- "anthropic.claude-3-haiku-20240307-v1:0"
- when "claude-3-sonnet"
- "anthropic.claude-3-sonnet-20240229-v1:0"
- when "claude-instant-1"
- "anthropic.claude-instant-v1"
- when "claude-3-opus"
- "anthropic.claude-3-opus-20240229-v1:0"
- end
- end
-
- def display_name(model)
- case model
- when "claude-2"
- "Claude 2"
- when "claude-3-haiku"
- "Claude 3 Haiku"
- when "claude-3-sonnet"
- "Claude 3 Sonnet"
- when "claude-instant-1"
- "Claude Instant 1"
- when "claude-3-opus"
- "Claude 3 Opus"
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240603133432_seed_other_propietary_models.rb b/db/migrate/20240603133432_seed_other_propietary_models.rb
deleted file mode 100644
index 085ca41a..00000000
--- a/db/migrate/20240603133432_seed_other_propietary_models.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-class SeedOtherPropietaryModels < ActiveRecord::Migration[7.0]
- def up
- models = []
-
- gemini_key = fetch_setting("ai_gemini_api_key")
- enabled_models = fetch_setting("ai_bot_enabled_chat_bots")&.split("|").to_a
-
- if gemini_key.present?
- gemini_models = %w[gemini-pro gemini-1.5-pro gemini-1.5-flash]
-
- gemini_models.each do |gm|
- url = "https://generativelanguage.googleapis.com/v1beta/models/#{gemini_mapped_model(gm)}"
-
- bot_user_id = "NULL"
- bot_user_id = -115 if gm == "gemini-1.5-pro" && has_companion_user?(-115)
-
- enabled = enabled_models.include?(gm)
- models << "('#{gm.titleize}', '#{gm}', 'google', 'DiscourseAi::Tokenizer::OpenAiTokenizer', '#{gemini_tokens(gm)}', '#{url}', '#{gemini_key}', #{bot_user_id}, #{enabled}, NOW(), NOW())"
- end
- end
-
- cohere_key = fetch_setting("ai_cohere_api_key")
-
- if cohere_key.present?
- cohere_models = %w[command-light command command-r command-r-plus]
-
- cohere_models.each do |cm|
- bot_user_id = "NULL"
- bot_user_id = -120 if cm == "command-r-plus" && has_companion_user?(-120)
-
- enabled = enabled_models.include?(cm)
- models << "('#{cm.titleize}', '#{cm}', 'cohere', 'DiscourseAi::Tokenizer::OpenAiTokenizer', #{cohere_tokens(cm)}, 'https://api.cohere.ai/v1/chat', '#{cohere_key}', #{bot_user_id}, #{enabled}, NOW(), NOW())"
- end
- end
-
- if models.present?
- rows = models.compact.join(", ")
-
- DB.exec(<<~SQL, rows: rows) if rows.present?
- INSERT INTO llm_models(display_name, name, provider, tokenizer, max_prompt_tokens, url, api_key, user_id, enabled_chat_bot, created_at, updated_at)
- VALUES #{rows};
- SQL
- end
- end
-
- def has_companion_user?(user_id)
- DB.query_single("SELECT id FROM users WHERE id = :user_id", user_id: user_id).first.present?
- end
-
- def fetch_setting(name)
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: name,
- ).first
- end
-
- def cohere_tokens(model)
- case model
- when "command-light"
- 4096
- when "command"
- 8192
- when "command-r"
- 131_072
- when "command-r-plus"
- 131_072
- else
- 8192
- end
- end
-
- def gemini_mapped_model(model)
- case model
- when "gemini-1.5-pro"
- "gemini-1.5-pro-latest"
- when "gemini-1.5-flash"
- "gemini-1.5-flash-latest"
- else
- "gemini-pro-latest"
- end
- end
-
- def gemini_tokens(model)
- if model.start_with?("gemini-1.5")
- # technically we support 1 million tokens, but we're being conservative
- 800_000
- else
- 16_384 # 50% of model tokens
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240603143158_seed_oss_models.rb b/db/migrate/20240603143158_seed_oss_models.rb
deleted file mode 100644
index a528fac9..00000000
--- a/db/migrate/20240603143158_seed_oss_models.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-class SeedOssModels < ActiveRecord::Migration[7.0]
- def up
- models = []
- enabled_models = fetch_setting("ai_bot_enabled_chat_bots")&.split("|").to_a
- enabled = enabled_models.include?("mixtral-8x7B-Instruct-V0.1")
-
- hf_key = fetch_setting("ai_hugging_face_api_key")
- hf_url = fetch_setting("ai_hugging_face_api_url")
-
- user_id = has_companion_user?(-114) ? -114 : "NULL"
-
- if hf_url.present? && hf_key.present?
- hf_token_limit = fetch_setting("ai_hugging_face_token_limit")
- hf_display_name = fetch_setting("ai_hugging_face_model_display_name")
-
- name = hf_display_name || "mistralai/Mixtral"
- token_limit = hf_token_limit || 32_000
-
- models << "('#{name}', '#{name}', 'hugging_face', 'DiscourseAi::Tokenizer::MixtralTokenizer', #{token_limit}, '#{hf_url}', '#{hf_key}', #{user_id}, #{enabled}, NOW(), NOW())"
- end
-
- vllm_key = fetch_setting("ai_vllm_api_key")
- vllm_url = fetch_setting("ai_vllm_endpoint")
-
- if vllm_key.present? && vllm_url.present?
- url = "#{vllm_url}/v1/chat/completions"
- name = "mistralai/Mixtral"
-
- models << "('#{name}', '#{name}', 'vllm', 'DiscourseAi::Tokenizer::MixtralTokenizer', 32000, '#{url}', '#{vllm_key}', #{user_id}, #{enabled}, NOW(), NOW())"
- end
-
- vllm_srv = fetch_setting("ai_vllm_endpoint_srv")
- srv_reserved_url = "https://vllm.shadowed-by-srv.invalid"
-
- srv_record =
- DB.query_single(
- "SELECT id FROM llm_models WHERE url = :reserved",
- reserved: srv_reserved_url,
- ).first
-
- if vllm_srv.present? && srv_record.nil?
- url = "https://vllm.shadowed-by-srv.invalid"
- name = "mistralai/Mixtral"
-
- models << "('vLLM SRV LLM', '#{name}', 'vllm', 'DiscourseAi::Tokenizer::MixtralTokenizer', 32000, '#{url}', '#{vllm_key}', #{user_id}, #{enabled}, NOW(), NOW())"
- end
-
- if models.present?
- rows = models.compact.join(", ")
-
- DB.exec(<<~SQL, rows: rows) if rows.present?
- INSERT INTO llm_models(display_name, name, provider, tokenizer, max_prompt_tokens, url, api_key, user_id, enabled_chat_bot, created_at, updated_at)
- VALUES #{rows};
- SQL
- end
- end
-
- def has_companion_user?(user_id)
- DB.query_single("SELECT id FROM users WHERE id = :user_id", user_id: user_id).first.present?
- end
-
- def fetch_setting(name)
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: name,
- ).first
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240606151348_create_ai_summaries_table.rb b/db/migrate/20240606151348_create_ai_summaries_table.rb
deleted file mode 100644
index bcace654..00000000
--- a/db/migrate/20240606151348_create_ai_summaries_table.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class CreateAiSummariesTable < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_summaries do |t|
- t.integer :target_id, null: false
- t.string :target_type, null: false
- t.int4range :content_range
- t.string :summarized_text, null: false
- t.string :original_content_sha, null: false
- t.string :algorithm, null: false
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20240606152117_copy_summary_sections_to_ai_summaries.rb b/db/migrate/20240606152117_copy_summary_sections_to_ai_summaries.rb
deleted file mode 100644
index 326f78cc..00000000
--- a/db/migrate/20240606152117_copy_summary_sections_to_ai_summaries.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-class CopySummarySectionsToAiSummaries < ActiveRecord::Migration[7.0]
- def up
- execute <<-SQL
- INSERT INTO ai_summaries (id, target_id, target_type, content_range, summarized_text, original_content_sha, algorithm, created_at, updated_at)
- SELECT id, target_id, target_type, content_range, summarized_text, original_content_sha, algorithm, created_at, updated_at
- FROM summary_sections
- WHERE meta_section_id IS NULL
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240609061418_tool_details_and_command_removal.rb b/db/migrate/20240609061418_tool_details_and_command_removal.rb
deleted file mode 100644
index f4db4f75..00000000
--- a/db/migrate/20240609061418_tool_details_and_command_removal.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-class ToolDetailsAndCommandRemoval < ActiveRecord::Migration[7.0]
- def change
- add_column :ai_personas, :tool_details, :boolean, default: true, null: false
- add_column :ai_personas, :tools, :json, null: false, default: []
- # we can not do this cause we are seeding the data and in certain
- # build scenarios we seed prior to running post migrations
- # this risks potentially dropping data but the window is small
- # Migration::ColumnDropper.mark_readonly(:ai_personas, :commands)
-
- execute <<~SQL
- UPDATE ai_personas
- SET tools = commands
- SQL
- end
-end
diff --git a/db/migrate/20240609232736_drop_commands_from_ai_personas.rb b/db/migrate/20240609232736_drop_commands_from_ai_personas.rb
deleted file mode 100644
index 1a3ae897..00000000
--- a/db/migrate/20240609232736_drop_commands_from_ai_personas.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-class DropCommandsFromAiPersonas < ActiveRecord::Migration[7.0]
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-
- def up
- Migration::ColumnDropper.execute_drop(:ai_personas, [:commands])
- end
-end
diff --git a/db/migrate/20240610232040_copy_summarization_strategy_to_ai_summarization_strategy.rb b/db/migrate/20240610232040_copy_summarization_strategy_to_ai_summarization_strategy.rb
deleted file mode 100644
index bbad5955..00000000
--- a/db/migrate/20240610232040_copy_summarization_strategy_to_ai_summarization_strategy.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-class CopySummarizationStrategyToAiSummarizationStrategy < ActiveRecord::Migration[7.0]
- def up
- execute <<-SQL
- UPDATE site_settings
- SET data_type = (SELECT data_type FROM site_settings WHERE name = 'summarization_strategy'),
- value = (SELECT value FROM site_settings WHERE name = 'summarization_strategy')
- WHERE name = 'ai_summarization_strategy'
- AND EXISTS (SELECT 1 FROM site_settings WHERE name = 'summarization_strategy');
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240610232546_copy_custom_summarization_allowed_groups_to_ai_custom_summarization_allowed_groups.rb b/db/migrate/20240610232546_copy_custom_summarization_allowed_groups_to_ai_custom_summarization_allowed_groups.rb
deleted file mode 100644
index ae28e389..00000000
--- a/db/migrate/20240610232546_copy_custom_summarization_allowed_groups_to_ai_custom_summarization_allowed_groups.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-class CopyCustomSummarizationAllowedGroupsToAiCustomSummarizationAllowedGroups < ActiveRecord::Migration[
- 7.0
-]
- def up
- execute <<-SQL
- UPDATE site_settings
- SET data_type = (SELECT data_type FROM site_settings WHERE name = 'custom_summarization_allowed_groups'),
- value = (SELECT value FROM site_settings WHERE name = 'custom_summarization_allowed_groups')
- WHERE name = 'ai_custom_summarization_allowed_groups'
- AND EXISTS (SELECT 1 FROM site_settings WHERE name = 'custom_summarization_allowed_groups');
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240611170904_upgrade_pgvector_070.rb b/db/migrate/20240611170904_upgrade_pgvector_070.rb
deleted file mode 100644
index 875b5362..00000000
--- a/db/migrate/20240611170904_upgrade_pgvector_070.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-class UpgradePgvector070 < ActiveRecord::Migration[7.0]
- def up
- minimum_target_version = "0.7.0"
- installed_version =
- DB.query_single("SELECT extversion FROM pg_extension WHERE extname = 'vector';").first
-
- if Gem::Version.new(installed_version) < Gem::Version.new(minimum_target_version)
- DB.exec("ALTER EXTENSION vector UPDATE TO '0.7.0';")
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240611170905_move_embeddings_to_single_table_per_type.rb b/db/migrate/20240611170905_move_embeddings_to_single_table_per_type.rb
deleted file mode 100644
index fad7070b..00000000
--- a/db/migrate/20240611170905_move_embeddings_to_single_table_per_type.rb
+++ /dev/null
@@ -1,159 +0,0 @@
-# frozen_string_literal: true
-
-class MoveEmbeddingsToSingleTablePerType < ActiveRecord::Migration[7.0]
- def up
- create_table :ai_topic_embeddings, id: false do |t|
- t.integer :topic_id, null: false
- t.integer :model_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_id, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "halfvec", null: false
- t.timestamps
-
- t.index %i[model_id strategy_id topic_id],
- unique: true,
- name: "index_ai_topic_embeddings_on_model_strategy_topic"
- end
-
- create_table :ai_post_embeddings, id: false do |t|
- t.integer :post_id, null: false
- t.integer :model_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_id, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "halfvec", null: false
- t.timestamps
-
- t.index %i[model_id strategy_id post_id],
- unique: true,
- name: "index_ai_post_embeddings_on_model_strategy_post"
- end
-
- create_table :ai_document_fragment_embeddings, id: false do |t|
- t.integer :rag_document_fragment_id, null: false
- t.integer :model_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_id, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "halfvec", null: false
- t.timestamps
-
- t.index %i[model_id strategy_id rag_document_fragment_id],
- unique: true,
- name: "index_ai_fragment_embeddings_on_model_strategy_fragment"
- end
-
- # Copy data from old tables to new tables
- execute <<-SQL
- INSERT INTO ai_topic_embeddings (topic_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT topic_id, 1, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_topic_embeddings_1_1;
-
- INSERT INTO ai_topic_embeddings (topic_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT topic_id, 2, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_topic_embeddings_2_1;
-
- INSERT INTO ai_topic_embeddings (topic_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT topic_id, 3, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_topic_embeddings_3_1;
-
- INSERT INTO ai_topic_embeddings (topic_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT topic_id, 4, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_topic_embeddings_4_1;
-
- INSERT INTO ai_topic_embeddings (topic_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT topic_id, 5, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_topic_embeddings_5_1;
-
- INSERT INTO ai_topic_embeddings (topic_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT topic_id, 6, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_topic_embeddings_6_1;
-
- INSERT INTO ai_topic_embeddings (topic_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT topic_id, 7, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_topic_embeddings_7_1;
-
- INSERT INTO ai_topic_embeddings (topic_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT topic_id, 8, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_topic_embeddings_8_1;
-
- INSERT INTO ai_post_embeddings (post_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT post_id, 1, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_post_embeddings_1_1;
-
- INSERT INTO ai_post_embeddings (post_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT post_id, 2, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_post_embeddings_2_1;
-
- INSERT INTO ai_post_embeddings (post_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT post_id, 3, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_post_embeddings_3_1;
-
- INSERT INTO ai_post_embeddings (post_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT post_id, 4, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_post_embeddings_4_1;
-
- INSERT INTO ai_post_embeddings (post_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT post_id, 5, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_post_embeddings_5_1;
-
- INSERT INTO ai_post_embeddings (post_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT post_id, 6, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_post_embeddings_6_1;
-
- INSERT INTO ai_post_embeddings (post_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT post_id, 7, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_post_embeddings_7_1;
-
- INSERT INTO ai_post_embeddings (post_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT post_id, 8, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_post_embeddings_8_1;
-
- INSERT INTO ai_document_fragment_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT rag_document_fragment_id, 1, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_document_fragment_embeddings_1_1;
-
- INSERT INTO ai_document_fragment_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT rag_document_fragment_id, 2, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_document_fragment_embeddings_2_1;
-
- INSERT INTO ai_document_fragment_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT rag_document_fragment_id, 3, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_document_fragment_embeddings_3_1;
-
- INSERT INTO ai_document_fragment_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT rag_document_fragment_id, 4, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_document_fragment_embeddings_4_1;
-
- INSERT INTO ai_document_fragment_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT rag_document_fragment_id, 5, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_document_fragment_embeddings_5_1;
-
- INSERT INTO ai_document_fragment_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT rag_document_fragment_id, 6, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_document_fragment_embeddings_6_1;
-
- INSERT INTO ai_document_fragment_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT rag_document_fragment_id, 7, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_document_fragment_embeddings_7_1;
-
- INSERT INTO ai_document_fragment_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT rag_document_fragment_id, 8, model_version, 1, strategy_version, digest, embeddings, created_at, updated_at
- FROM ai_document_fragment_embeddings_8_1;
- SQL
-
- begin
- DiscourseAi::Embeddings::VectorRepresentations::Base.current_representation
- rescue StandardError => e
- Rails.logger.error("Failed to index embeddings: #{e}")
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240611170906_drop_old_embeddings_tables.rb b/db/migrate/20240611170906_drop_old_embeddings_tables.rb
deleted file mode 100644
index 327f158a..00000000
--- a/db/migrate/20240611170906_drop_old_embeddings_tables.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-class DropOldEmbeddingsTables < ActiveRecord::Migration[7.0]
- def up
- drop_table :ai_topic_embeddings_1_1
- drop_table :ai_topic_embeddings_2_1
- drop_table :ai_topic_embeddings_3_1
- drop_table :ai_topic_embeddings_4_1
- drop_table :ai_topic_embeddings_5_1
- drop_table :ai_topic_embeddings_6_1
- drop_table :ai_topic_embeddings_7_1
- drop_table :ai_topic_embeddings_8_1
- drop_table :ai_post_embeddings_1_1
- drop_table :ai_post_embeddings_2_1
- drop_table :ai_post_embeddings_3_1
- drop_table :ai_post_embeddings_4_1
- drop_table :ai_post_embeddings_5_1
- drop_table :ai_post_embeddings_6_1
- drop_table :ai_post_embeddings_7_1
- drop_table :ai_post_embeddings_8_1
- drop_table :ai_document_fragment_embeddings_1_1
- drop_table :ai_document_fragment_embeddings_2_1
- drop_table :ai_document_fragment_embeddings_3_1
- drop_table :ai_document_fragment_embeddings_4_1
- drop_table :ai_document_fragment_embeddings_5_1
- drop_table :ai_document_fragment_embeddings_6_1
- drop_table :ai_document_fragment_embeddings_7_1
- drop_table :ai_document_fragment_embeddings_8_1
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240618080148_create_ai_tools.rb b/db/migrate/20240618080148_create_ai_tools.rb
deleted file mode 100644
index 30d84def..00000000
--- a/db/migrate/20240618080148_create_ai_tools.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-class CreateAiTools < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_tools do |t|
- t.string :name, null: false, max_length: 100, unique: true
- t.string :description, null: false, max_length: 1000
-
- t.string :summary, null: false, max_length: 255
-
- t.jsonb :parameters, null: false, default: {}
- t.text :script, null: false, max_length: 100_000
- t.integer :created_by_id, null: false
-
- t.boolean :enabled, null: false, default: true
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20240619193057_choose_llm_model_setting_migration.rb b/db/migrate/20240619193057_choose_llm_model_setting_migration.rb
deleted file mode 100644
index 30015207..00000000
--- a/db/migrate/20240619193057_choose_llm_model_setting_migration.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-class ChooseLlmModelSettingMigration < ActiveRecord::Migration[7.0]
- def up
- transition_to_llm_model("ai_helper_model")
- transition_to_llm_model("ai_embeddings_semantic_search_hyde_model")
- end
-
- def transition_to_llm_model(llm_setting_name)
- setting_value =
- DB
- .query_single(
- "SELECT value FROM site_settings WHERE name = :llm_setting",
- llm_setting: llm_setting_name,
- )
- .first
- .to_s
-
- return if setting_value.empty?
-
- provider_and_model = setting_value.split(":")
- provider = provider_and_model.first
- model = provider_and_model.second
- return if provider == "custom"
-
- llm_model_id = DB.query_single(<<~SQL, provider: provider, model: model).first.to_s
- SELECT id FROM llm_models WHERE provider = :provider AND name = :model
- SQL
-
- return if llm_model_id.empty?
-
- DB.exec(<<~SQL, llm_setting: llm_setting_name, new_value: "custom:#{llm_model_id}")
- UPDATE site_settings SET value=:new_value WHERE name=:llm_setting
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240619211337_update_automation_script_models.rb b/db/migrate/20240619211337_update_automation_script_models.rb
deleted file mode 100644
index 53fe59be..00000000
--- a/db/migrate/20240619211337_update_automation_script_models.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-class UpdateAutomationScriptModels < ActiveRecord::Migration[7.0]
- def up
- script_names = %w[llm_triage llm_report]
-
- fields_to_update = DB.query(<<~SQL, script_names: script_names)
- SELECT fields.id, fields.metadata
- FROM discourse_automation_fields fields
- INNER JOIN discourse_automation_automations automations ON automations.id = fields.automation_id
- WHERE fields.name = 'model'
- AND automations.script IN (:script_names)
- SQL
-
- return if fields_to_update.empty?
-
- updated_fields =
- fields_to_update
- .map do |field|
- new_metadata = { "value" => translate_model(field.metadata["value"]) }.to_json
-
- "(#{field.id}, '#{new_metadata}')" if new_metadata.present?
- end
- .compact
-
- return if updated_fields.empty?
-
- DB.exec(<<~SQL)
- UPDATE discourse_automation_fields AS fields
- SET metadata = new_fields.metadata::jsonb
- FROM (VALUES #{updated_fields.join(", ")}) AS new_fields(id, metadata)
- WHERE new_fields.id::bigint = fields.id
- SQL
- end
-
- def translate_model(current_model)
- options = DB.query(<<~SQL, name: current_model.to_s).to_a
- SELECT id, provider
- FROM llm_models
- WHERE name = :name
- SQL
-
- return if options.empty?
- return "custom:#{options.first.id}" if options.length == 1
-
- priority_provider = options.find { |o| o.provider == "aws_bedrock" || o.provider == "vllm" }
-
- return "custom:#{priority_provider.id}" if priority_provider
-
- "custom:#{options.first.id}"
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240624135356_llm_model_custom_params.rb b/db/migrate/20240624135356_llm_model_custom_params.rb
deleted file mode 100644
index ce47b2c2..00000000
--- a/db/migrate/20240624135356_llm_model_custom_params.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class LlmModelCustomParams < ActiveRecord::Migration[7.0]
- def change
- add_column :llm_models, :provider_params, :jsonb
- end
-end
diff --git a/db/migrate/20240624202602_add_provider_specific_params_to_llm_models.rb b/db/migrate/20240624202602_add_provider_specific_params_to_llm_models.rb
deleted file mode 100644
index 10416ee1..00000000
--- a/db/migrate/20240624202602_add_provider_specific_params_to_llm_models.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-class AddProviderSpecificParamsToLlmModels < ActiveRecord::Migration[7.0]
- def up
- open_ai_organization = fetch_setting("ai_openai_organization")
-
- DB.exec(<<~SQL, organization: open_ai_organization) if open_ai_organization
- UPDATE llm_models
- SET provider_params = jsonb_build_object('organization', :organization)
- WHERE provider = 'open_ai' AND provider_params IS NULL
- SQL
-
- bedrock_region = fetch_setting("ai_bedrock_region") || "us-east-1"
- bedrock_access_key_id = fetch_setting("ai_bedrock_access_key_id")
-
- DB.exec(<<~SQL, key_id: bedrock_access_key_id, region: bedrock_region) if bedrock_access_key_id
- UPDATE llm_models
- SET
- provider_params = jsonb_build_object('access_key_id', :key_id, 'region', :region),
- name = CASE name WHEN 'claude-2' THEN 'anthropic.claude-v2:1'
- WHEN 'claude-3-haiku' THEN 'anthropic.claude-3-haiku-20240307-v1:0'
- WHEN 'claude-3-sonnet' THEN 'anthropic.claude-3-sonnet-20240229-v1:0'
- WHEN 'claude-instant-1' THEN 'anthropic.claude-instant-v1'
- WHEN 'claude-3-opus' THEN 'anthropic.claude-3-opus-20240229-v1:0'
- WHEN 'claude-3-5-sonnet' THEN 'anthropic.claude-3-5-sonnet-20240620-v1:0'
- ELSE name
- END
- WHERE provider = 'aws_bedrock' AND provider_params IS NULL
- SQL
- end
-
- def fetch_setting(name)
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: name,
- ).first
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240703135444_llm_models_for_summarization.rb b/db/migrate/20240703135444_llm_models_for_summarization.rb
deleted file mode 100644
index 5046139e..00000000
--- a/db/migrate/20240703135444_llm_models_for_summarization.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-# frozen_string_literal: true
-
-class LlmModelsForSummarization < ActiveRecord::Migration[7.0]
- def up
- setting_value =
- DB
- .query_single(
- "SELECT value FROM site_settings WHERE name = :llm_setting",
- llm_setting: "ai_summarization_strategy",
- )
- .first
- .to_s
-
- return if setting_value.empty?
-
- gpt_models = %w[gpt-4 gpt-4-32k gpt-4-turbo gpt-4o gpt-3.5-turbo gpt-3.5-turbo-16k]
- gemini_models = %w[gemini-pro gemini-1.5-pro gemini-1.5-flash]
- claude_models = %w[claude-2 claude-instant-1 claude-3-haiku claude-3-sonnet claude-3-opus]
- oss_models = %w[mistralai/Mixtral-8x7B-Instruct-v0.1 mistralai/Mixtral-8x7B-Instruct-v0.1]
-
- providers = []
- prov_priority = ""
-
- if gpt_models.include?(setting_value)
- providers = %w[azure open_ai]
- prov_priority = "azure"
- elsif gemini_models.include?(setting_value)
- providers = %w[google]
- prov_priority = "google"
- elsif claude_models.include?(setting_value)
- providers = %w[aws_bedrock anthropic]
- prov_priority = "aws_bedrock"
- elsif oss_models.include?(setting_value)
- providers = %w[hugging_face vllm]
- prov_priority = "vllm"
- end
-
- insert_llm_model(setting_value, providers, prov_priority) if providers.present?
- end
-
- def insert_llm_model(old_value, providers, priority)
- matching_models = DB.query(<<~SQL, model_name: old_value, providers: providers)
- SELECT * FROM llm_models WHERE name = :model_name AND provider IN (:providers)
- SQL
-
- return if matching_models.empty?
-
- priority_model = matching_models.find { |m| m.provider == priority } || matching_models.first
- new_value = "custom:#{priority_model.id}"
-
- DB.exec(<<~SQL, new_value: new_value)
- INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
- VALUES ('ai_summarization_model', 1, :new_value, NOW(), NOW())
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240704020102_reset_identity_on_ai_summary.rb b/db/migrate/20240704020102_reset_identity_on_ai_summary.rb
deleted file mode 100644
index 5c87763d..00000000
--- a/db/migrate/20240704020102_reset_identity_on_ai_summary.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-class ResetIdentityOnAiSummary < ActiveRecord::Migration[7.0]
- def up
- add_index :ai_summaries, %i[target_type target_id]
-
- # we need to reset identity since we moved this from the old summary_sections table
- execute <<-SQL
- DO $$
- DECLARE
- max_id integer;
- BEGIN
- SELECT MAX(id) INTO max_id FROM ai_summaries;
- IF max_id IS NOT NULL THEN
- PERFORM setval(pg_get_serial_sequence('ai_summaries', 'id'), max_id);
- END IF;
- END $$
- SQL
- end
-
- def down
- remove_index :ai_summaries, %i[target_type target_id]
- end
-end
diff --git a/db/migrate/20240708193243_fix_vllm_model_name.rb b/db/migrate/20240708193243_fix_vllm_model_name.rb
deleted file mode 100644
index b5d715ce..00000000
--- a/db/migrate/20240708193243_fix_vllm_model_name.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-class FixVllmModelName < ActiveRecord::Migration[7.1]
- def up
- vllm_mixtral_model_id = DB.query_single(<<~SQL).first
- SELECT id FROM llm_models WHERE name = 'mistralai/Mixtral'
- SQL
-
- DB.exec(<<~SQL, target_id: vllm_mixtral_model_id) if vllm_mixtral_model_id
- UPDATE llm_models
- SET name = 'mistralai/Mixtral-8x7B-Instruct-v0.1'
- WHERE id = :target_id
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240719143453_llm_model_vision_enabled.rb b/db/migrate/20240719143453_llm_model_vision_enabled.rb
deleted file mode 100644
index c2280818..00000000
--- a/db/migrate/20240719143453_llm_model_vision_enabled.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class LlmModelVisionEnabled < ActiveRecord::Migration[7.1]
- def change
- add_column :llm_models, :vision_enabled, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20240726164937_fix_ai_summaries_sequence.rb b/db/migrate/20240726164937_fix_ai_summaries_sequence.rb
deleted file mode 100644
index 918afc57..00000000
--- a/db/migrate/20240726164937_fix_ai_summaries_sequence.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-class FixAiSummariesSequence < ActiveRecord::Migration[7.0]
- def up
- begin
- execute <<-SQL
- SELECT
- SETVAL (
- 'ai_summaries_id_seq',
- (
- SELECT
- GREATEST (
- (
- SELECT
- MAX(id)
- FROM
- summary_sections
- ),
- (
- SELECT
- MAX(id)
- FROM
- ai_summaries
- )
- )
- ),
- true
- );
- SQL
- rescue ActiveRecord::StatementInvalid => e
- # if the summary_table does not exist, we can ignore the error
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20240807150605_add_default_to_provider_params.rb b/db/migrate/20240807150605_add_default_to_provider_params.rb
deleted file mode 100644
index 4363d0bb..00000000
--- a/db/migrate/20240807150605_add_default_to_provider_params.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class AddDefaultToProviderParams < ActiveRecord::Migration[7.1]
- def change
- change_column_default :llm_models, :provider_params, from: nil, to: {}
- end
-end
diff --git a/db/migrate/20240909180908_add_ai_summary_type_column.rb b/db/migrate/20240909180908_add_ai_summary_type_column.rb
deleted file mode 100644
index 82284106..00000000
--- a/db/migrate/20240909180908_add_ai_summary_type_column.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class AddAiSummaryTypeColumn < ActiveRecord::Migration[7.1]
- def change
- add_column :ai_summaries, :summary_type, :integer, default: 0, null: false
- end
-end
diff --git a/db/migrate/20240912052713_add_target_to_rag_document_fragment.rb b/db/migrate/20240912052713_add_target_to_rag_document_fragment.rb
deleted file mode 100644
index 43b48ad4..00000000
--- a/db/migrate/20240912052713_add_target_to_rag_document_fragment.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class AddTargetToRagDocumentFragment < ActiveRecord::Migration[7.1]
- def change
- add_column :rag_document_fragments, :target_id, :integer, null: true
- add_column :rag_document_fragments, :target_type, :string, limit: 800, null: true
- add_index :rag_document_fragments, %i[target_type target_id]
- end
-end
diff --git a/db/migrate/20240913054440_add_rag_columns_to_ai_tools.rb b/db/migrate/20240913054440_add_rag_columns_to_ai_tools.rb
deleted file mode 100644
index 4ec6100b..00000000
--- a/db/migrate/20240913054440_add_rag_columns_to_ai_tools.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-class AddRagColumnsToAiTools < ActiveRecord::Migration[7.1]
- def change
- add_column :ai_tools, :rag_chunk_tokens, :integer, null: false, default: 374
- add_column :ai_tools, :rag_chunk_overlap_tokens, :integer, null: false, default: 10
- end
-end
diff --git a/db/migrate/20241008054440_create_binary_indexes_for_embeddings.rb b/db/migrate/20241008054440_create_binary_indexes_for_embeddings.rb
deleted file mode 100644
index aa31a4e3..00000000
--- a/db/migrate/20241008054440_create_binary_indexes_for_embeddings.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-class CreateBinaryIndexesForEmbeddings < ActiveRecord::Migration[7.1]
- def up
- %w[topic post document_fragment].each do |type|
- # our supported embeddings models IDs and dimensions
- [
- [1, 768],
- [2, 1536],
- [3, 1024],
- [4, 1024],
- [5, 768],
- [6, 1536],
- [7, 2000],
- [8, 1024],
- ].each { |model_id, dimensions| execute <<-SQL }
- CREATE INDEX ai_#{type}_embeddings_#{model_id}_1_search_bit ON ai_#{type}_embeddings
- USING hnsw ((binary_quantize(embeddings)::bit(#{dimensions})) bit_hamming_ops)
- WHERE model_id = #{model_id} AND strategy_id = 1;
- SQL
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20241009230724_add_forced_tool_count_to_ai_personas.rb b/db/migrate/20241009230724_add_forced_tool_count_to_ai_personas.rb
deleted file mode 100644
index 23a0496d..00000000
--- a/db/migrate/20241009230724_add_forced_tool_count_to_ai_personas.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class AddForcedToolCountToAiPersonas < ActiveRecord::Migration[7.1]
- def change
- add_column :ai_personas, :forced_tool_count, :integer, default: -1, null: false
- end
-end
diff --git a/db/migrate/20241014010245_ai_persona_chat_topic_refactor.rb b/db/migrate/20241014010245_ai_persona_chat_topic_refactor.rb
deleted file mode 100644
index 9a7e9a88..00000000
--- a/db/migrate/20241014010245_ai_persona_chat_topic_refactor.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-class AiPersonaChatTopicRefactor < ActiveRecord::Migration[7.1]
- def change
- add_column :ai_personas, :allow_chat_channel_mentions, :boolean, default: false, null: false
- add_column :ai_personas, :allow_chat_direct_messages, :boolean, default: false, null: false
- add_column :ai_personas, :allow_topic_mentions, :boolean, default: false, null: false
- add_column :ai_personas, :allow_personal_messages, :boolean, default: true, null: false
- add_column :ai_personas, :force_default_llm, :boolean, default: false, null: false
-
- execute <<~SQL
- UPDATE ai_personas
- SET allow_chat_channel_mentions = mentionable, allow_chat_direct_messages = true
- WHERE allow_chat = true
- SQL
-
- execute <<~SQL
- UPDATE ai_personas
- SET allow_topic_mentions = true
- WHERE mentionable = true
- SQL
- end
-end
diff --git a/db/migrate/20241020010245_add_tool_name_to_ai_tools.rb b/db/migrate/20241020010245_add_tool_name_to_ai_tools.rb
deleted file mode 100644
index ab9a549f..00000000
--- a/db/migrate/20241020010245_add_tool_name_to_ai_tools.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-class AddToolNameToAiTools < ActiveRecord::Migration[7.1]
- def up
- add_column :ai_tools,
- :tool_name,
- :string,
- null: false,
- limit: 100,
- default: "",
- if_not_exists: true
-
- # Migrate existing name to tool_name
- execute <<~SQL
- UPDATE ai_tools
- SET tool_name = regexp_replace(LOWER(name),'[^a-z0-9_]','', 'g');
- SQL
- end
-
- def down
- remove_column :ai_tools, :tool_name, if_exists: true
- end
-end
diff --git a/db/migrate/20241023033955_add_feature_context_to_ai_api_log.rb b/db/migrate/20241023033955_add_feature_context_to_ai_api_log.rb
deleted file mode 100644
index 5191541c..00000000
--- a/db/migrate/20241023033955_add_feature_context_to_ai_api_log.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-#
-class AddFeatureContextToAiApiLog < ActiveRecord::Migration[7.1]
- def change
- add_column :ai_api_audit_logs, :feature_context, :jsonb
- end
-end
diff --git a/db/migrate/20241023041242_add_unique_constraint_to_ai_tools.rb b/db/migrate/20241023041242_add_unique_constraint_to_ai_tools.rb
deleted file mode 100644
index 2c1a37a0..00000000
--- a/db/migrate/20241023041242_add_unique_constraint_to_ai_tools.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-class AddUniqueConstraintToAiTools < ActiveRecord::Migration[7.1]
- def up
- # We need to remove duplicates before adding the unique constraint
- execute <<~SQL
- WITH duplicates AS (
- SELECT name, COUNT(*) as count, MIN(id) as keeper_id
- FROM ai_tools
- GROUP BY name
- HAVING COUNT(*) > 1
- )
- UPDATE ai_tools AS p
- SET name = CONCAT(p.name, p.id)
- FROM duplicates d
- WHERE p.name = d.name
- AND p.id != d.keeper_id;
- SQL
-
- add_index :ai_personas, :name, unique: true, if_not_exists: true
- end
-
- def down
- remove_index :ai_personas, :name, if_exists: true
- end
-end
diff --git a/db/migrate/20241025135522_alter_ai_ids_to_bigint.rb b/db/migrate/20241025135522_alter_ai_ids_to_bigint.rb
deleted file mode 100644
index b94d0b4d..00000000
--- a/db/migrate/20241025135522_alter_ai_ids_to_bigint.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-class AlterAiIdsToBigint < ActiveRecord::Migration[7.1]
- def up
- change_column :ai_document_fragment_embeddings, :rag_document_fragment_id, :bigint
- change_column :classification_results, :target_id, :bigint
- change_column :rag_document_fragments, :target_id, :bigint
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20241028034232_add_unique_ai_stream_conversation_user_id_index.rb b/db/migrate/20241028034232_add_unique_ai_stream_conversation_user_id_index.rb
deleted file mode 100644
index cff38477..00000000
--- a/db/migrate/20241028034232_add_unique_ai_stream_conversation_user_id_index.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-class AddUniqueAiStreamConversationUserIdIndex < ActiveRecord::Migration[7.1]
- def change
- add_index :user_custom_fields,
- [:value],
- unique: true,
- where: "name = 'ai-stream-conversation-unique-id'"
- end
-end
diff --git a/db/migrate/20241031145203_track_ai_summary_origin.rb b/db/migrate/20241031145203_track_ai_summary_origin.rb
deleted file mode 100644
index 25b571c2..00000000
--- a/db/migrate/20241031145203_track_ai_summary_origin.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class TrackAiSummaryOrigin < ActiveRecord::Migration[7.1]
- def change
- add_column :ai_summaries, :origin, :integer
- end
-end
diff --git a/db/migrate/20241031180044_set_origin_for_existing_ai_summaries.rb b/db/migrate/20241031180044_set_origin_for_existing_ai_summaries.rb
deleted file mode 100644
index 82c67f92..00000000
--- a/db/migrate/20241031180044_set_origin_for_existing_ai_summaries.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-class SetOriginForExistingAiSummaries < ActiveRecord::Migration[7.1]
- def up
- DB.exec <<~SQL
- UPDATE ai_summaries
- SET origin = CASE WHEN summary_type = 0 THEN 0 ELSE 1 END
- WHERE origin IS NULL
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20241104053017_add_ai_artifacts.rb b/db/migrate/20241104053017_add_ai_artifacts.rb
deleted file mode 100644
index 3ca78927..00000000
--- a/db/migrate/20241104053017_add_ai_artifacts.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-class AddAiArtifacts < ActiveRecord::Migration[7.1]
- def change
- create_table :ai_artifacts do |t|
- t.integer :user_id, null: false
- t.integer :post_id, null: false
- t.string :name, null: false, limit: 255
- t.string :html, limit: 65_535 # ~64KB limit
- t.string :css, limit: 65_535 # ~64KB limit
- t.string :js, limit: 65_535 # ~64KB limit
- t.jsonb :metadata # For any additional properties
-
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20241125132452_unique_ai_summaries.rb b/db/migrate/20241125132452_unique_ai_summaries.rb
deleted file mode 100644
index 08681201..00000000
--- a/db/migrate/20241125132452_unique_ai_summaries.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-class UniqueAiSummaries < ActiveRecord::Migration[7.1]
- def up
- execute <<~SQL
- DELETE FROM ai_summaries ais1
- USING ai_summaries ais2
- WHERE ais1.id < ais2.id
- AND ais1.target_id = ais2.target_id
- AND ais1.target_type = ais2.target_type
- AND ais1.summary_type = ais2.summary_type
- SQL
-
- add_index :ai_summaries, %i[target_id target_type summary_type], unique: true
- end
-
- def down
- remove_index :ai_summaries, column: %i[target_id target_type summary_type]
- end
-end
diff --git a/db/migrate/20241126033812_rename_ai_gist_batch_setting.rb b/db/migrate/20241126033812_rename_ai_gist_batch_setting.rb
deleted file mode 100644
index 196887e5..00000000
--- a/db/migrate/20241126033812_rename_ai_gist_batch_setting.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class RenameAiGistBatchSetting < ActiveRecord::Migration[7.0]
- def up
- execute "UPDATE site_settings SET name = 'ai_summary_gists_allowed_groups' WHERE name = 'ai_hot_topic_gists_allowed_groups'"
- end
-
- def down
- execute "UPDATE site_settings SET name = 'ai_hot_topic_gists_allowed_groups' WHERE name = 'ai_summary_gists_allowed_groups'"
- end
-end
diff --git a/db/migrate/20241128010221_add_cached_tokens_to_ai_api_audit_log.rb b/db/migrate/20241128010221_add_cached_tokens_to_ai_api_audit_log.rb
deleted file mode 100644
index a100bb84..00000000
--- a/db/migrate/20241128010221_add_cached_tokens_to_ai_api_audit_log.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class AddCachedTokensToAiApiAuditLog < ActiveRecord::Migration[7.2]
- def change
- add_column :ai_api_audit_logs, :cached_tokens, :integer
- add_index :ai_api_audit_logs, %i[created_at feature_name]
- add_index :ai_api_audit_logs, %i[created_at language_model]
- end
-end
diff --git a/db/migrate/20241129190708_fix_classification_data.rb b/db/migrate/20241129190708_fix_classification_data.rb
deleted file mode 100644
index 5e91b6e2..00000000
--- a/db/migrate/20241129190708_fix_classification_data.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-class FixClassificationData < ActiveRecord::Migration[7.2]
- def up
- classifications = DB.query(<<~SQL)
- SELECT id, classification
- FROM classification_results
- WHERE classification_type = 'sentiment'
- AND SUBSTRING(LTRIM(classification::text), 1, 1) = '['
- SQL
-
- transformed =
- classifications.reduce([]) do |memo, c|
- hash_result = {}
- c.classification.each { |r| hash_result[r["label"]] = r["score"] }
-
- memo << { id: c.id, fixed_classification: hash_result }
- end
-
- transformed_json = transformed.to_json
-
- DB.exec(<<~SQL, values: transformed_json)
- UPDATE classification_results
- SET classification = N.fixed_classification
- FROM (
- SELECT (value::jsonb->'id')::integer AS id, (value::jsonb->'fixed_classification')::jsonb AS fixed_classification
- FROM jsonb_array_elements(:values::jsonb)
- ) N
- WHERE classification_results.id = N.id
- AND classification_type = 'sentiment'
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20241130003808_add_artifact_versions.rb b/db/migrate/20241130003808_add_artifact_versions.rb
deleted file mode 100644
index bdab7155..00000000
--- a/db/migrate/20241130003808_add_artifact_versions.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-class AddArtifactVersions < ActiveRecord::Migration[7.0]
- def change
- create_table :ai_artifact_versions do |t|
- t.bigint :ai_artifact_id, null: false
- t.integer :version_number, null: false
- t.string :html, limit: 65_535
- t.string :css, limit: 65_535
- t.string :js, limit: 65_535
- t.jsonb :metadata
- t.string :change_description
- t.timestamps
-
- t.index %i[ai_artifact_id version_number], unique: true
- end
- end
-end
diff --git a/db/migrate/20241206030229_add_ai_moderation_settings.rb b/db/migrate/20241206030229_add_ai_moderation_settings.rb
deleted file mode 100644
index 12e17782..00000000
--- a/db/migrate/20241206030229_add_ai_moderation_settings.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-class AddAiModerationSettings < ActiveRecord::Migration[7.2]
- def change
- create_enum :ai_moderation_setting_type, %w[spam nsfw custom]
-
- create_table :ai_moderation_settings do |t|
- t.enum :setting_type, enum_type: "ai_moderation_setting_type", null: false
- t.jsonb :data, default: {}
- t.bigint :llm_model_id, null: false
- t.timestamps
- end
-
- add_index :ai_moderation_settings, :setting_type, unique: true
- end
-end
diff --git a/db/migrate/20241206051225_add_ai_spam_logs.rb b/db/migrate/20241206051225_add_ai_spam_logs.rb
deleted file mode 100644
index 5ef42388..00000000
--- a/db/migrate/20241206051225_add_ai_spam_logs.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-class AddAiSpamLogs < ActiveRecord::Migration[7.2]
- def change
- create_table :ai_spam_logs do |t|
- t.bigint :post_id, null: false
- t.bigint :llm_model_id, null: false
- t.bigint :ai_api_audit_log_id
- t.bigint :reviewable_id
- t.boolean :is_spam, null: false
- t.string :payload, null: false, default: "", limit: 20_000
- t.timestamps
- end
-
- add_index :ai_spam_logs, :post_id
- end
-end
diff --git a/db/migrate/20241217164540_create_embedding_definitions.rb b/db/migrate/20241217164540_create_embedding_definitions.rb
deleted file mode 100644
index 517ef407..00000000
--- a/db/migrate/20241217164540_create_embedding_definitions.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-class CreateEmbeddingDefinitions < ActiveRecord::Migration[7.2]
- def change
- create_table :embedding_definitions do |t|
- t.string :display_name, null: false
- t.integer :dimensions, null: false
- t.integer :max_sequence_length, null: false
- t.integer :version, null: false, default: 1
- t.string :pg_function, null: false
- t.string :provider, null: false
- t.string :tokenizer_class, null: false
- t.string :url, null: false
- t.string :api_key
- t.boolean :seeded, null: false, default: false
- t.jsonb :provider_params
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20241230153300_new_embeddings_tables.rb b/db/migrate/20241230153300_new_embeddings_tables.rb
deleted file mode 100644
index dacc148f..00000000
--- a/db/migrate/20241230153300_new_embeddings_tables.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-class NewEmbeddingsTables < ActiveRecord::Migration[7.2]
- def up
- create_table :ai_topics_embeddings, id: false do |t|
- t.bigint :topic_id, null: false
- t.bigint :model_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_id, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "halfvec", null: false
- t.timestamps
-
- t.index %i[model_id strategy_id topic_id],
- unique: true,
- name: "index_ai_topics_embeddings_on_model_strategy_topic"
- end
-
- create_table :ai_posts_embeddings, id: false do |t|
- t.bigint :post_id, null: false
- t.bigint :model_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_id, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "halfvec", null: false
- t.timestamps
-
- t.index %i[model_id strategy_id post_id],
- unique: true,
- name: "index_ai_posts_embeddings_on_model_strategy_post"
- end
-
- create_table :ai_document_fragments_embeddings, id: false do |t|
- t.bigint :rag_document_fragment_id, null: false
- t.bigint :model_id, null: false
- t.integer :model_version, null: false
- t.integer :strategy_id, null: false
- t.integer :strategy_version, null: false
- t.text :digest, null: false
- t.column :embeddings, "halfvec", null: false
- t.timestamps
-
- t.index %i[model_id strategy_id rag_document_fragment_id],
- unique: true,
- name: "index_ai_fragments_embeddings_on_model_strategy_fragment"
- end
-
- # Copied from 20241008054440_create_binary_indexes_for_embeddings
- %w[topics posts document_fragments].each do |type|
- # our supported embeddings models IDs and dimensions
- [
- [1, 768],
- [2, 1536],
- [3, 1024],
- [4, 1024],
- [5, 768],
- [6, 1536],
- [7, 2000],
- [8, 1024],
- ].each { |model_id, dimensions| execute <<-SQL }
- CREATE INDEX ai_#{type}_embeddings_#{model_id}_1_search_bit ON ai_#{type}_embeddings
- USING hnsw ((binary_quantize(embeddings)::bit(#{dimensions})) bit_hamming_ops)
- WHERE model_id = #{model_id} AND strategy_id = 1;
- SQL
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250102035341_add_llm_quota_tables.rb b/db/migrate/20250102035341_add_llm_quota_tables.rb
deleted file mode 100644
index 87a4f49c..00000000
--- a/db/migrate/20250102035341_add_llm_quota_tables.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-class AddLlmQuotaTables < ActiveRecord::Migration[7.2]
- def change
- create_table :llm_quotas do |t|
- t.bigint :group_id, null: false
- t.bigint :llm_model_id, null: false
- t.integer :max_tokens
- t.integer :max_usages
- t.integer :duration_seconds, null: false
- t.timestamps
- end
-
- add_index :llm_quotas, :llm_model_id
- add_index :llm_quotas, %i[group_id llm_model_id], unique: true
-
- create_table :llm_quota_usages do |t|
- t.bigint :user_id, null: false
- t.bigint :llm_quota_id, null: false
- t.integer :input_tokens_used, null: false
- t.integer :output_tokens_used, null: false
- t.integer :usages, null: false
- t.datetime :started_at, null: false
- t.datetime :reset_at, null: false
- t.timestamps
- end
-
- add_index :llm_quota_usages, :llm_quota_id
- add_index :llm_quota_usages, %i[user_id llm_quota_id], unique: true
- end
-end
diff --git a/db/migrate/20250110114305_embedding_config_data_migration.rb b/db/migrate/20250110114305_embedding_config_data_migration.rb
deleted file mode 100644
index acab9a57..00000000
--- a/db/migrate/20250110114305_embedding_config_data_migration.rb
+++ /dev/null
@@ -1,204 +0,0 @@
-# frozen_string_literal: true
-
-class EmbeddingConfigDataMigration < ActiveRecord::Migration[7.0]
- def up
- current_model = fetch_setting("ai_embeddings_model") || "bge-large-en"
- provider = provider_for(current_model)
-
- if provider.present?
- attrs = creds_for(provider)
-
- if attrs.present?
- attrs = attrs.merge(model_attrs(current_model))
- attrs[:display_name] = current_model
- attrs[:provider] = provider
- persist_config(attrs)
- end
- end
- end
-
- def down
- end
-
- # Utils
-
- def fetch_setting(name)
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: name,
- ).first || ENV["DISCOURSE_#{name&.upcase}"]
- end
-
- def provider_for(model)
- cloudflare_api_token = fetch_setting("ai_cloudflare_workers_api_token")
-
- return "cloudflare" if model == "bge-large-en" && cloudflare_api_token.present?
-
- tei_models = %w[bge-large-en bge-m3 multilingual-e5-large]
- return "hugging_face" if tei_models.include?(model)
-
- return "google" if model == "gemini"
-
- if %w[text-embedding-3-large text-embedding-3-small text-embedding-ada-002].include?(model)
- return "open_ai"
- end
-
- nil
- end
-
- def creds_for(provider)
- # CF
- if provider == "cloudflare"
- api_key = fetch_setting("ai_cloudflare_workers_api_token")
- account_id = fetch_setting("ai_cloudflare_workers_account_id")
-
- return if api_key.blank? || account_id.blank?
-
- {
- url:
- "https://api.cloudflare.com/client/v4/accounts/#{account_id}/ai/run/@cf/baai/bge-large-en-v1.5",
- api_key: api_key,
- }
- # TEI
- elsif provider == "hugging_face"
- seeded = false
- endpoint = fetch_setting("ai_hugging_face_tei_endpoint")
-
- if endpoint.blank?
- endpoint = fetch_setting("ai_hugging_face_tei_endpoint_srv")
- if endpoint.present?
- endpoint = "srv://#{endpoint}"
- seeded = true
- end
- end
-
- api_key = fetch_setting("ai_hugging_face_tei_api_key")
-
- return if endpoint.blank? || api_key.blank?
-
- { url: endpoint, api_key: api_key, seeded: seeded }
- # Gemini
- elsif provider == "google"
- api_key = fetch_setting("ai_gemini_api_key")
-
- return if api_key.blank?
-
- {
- url: "https://generativelanguage.googleapis.com/v1beta/models/embedding-001:embedContent",
- api_key: api_key,
- }
-
- # Open AI
- elsif provider == "open_ai"
- endpoint = fetch_setting("ai_openai_embeddings_url") || "https://api.openai.com/v1/embeddings"
- api_key = fetch_setting("ai_openai_api_key")
-
- return if endpoint.blank? || api_key.blank?
-
- { url: endpoint, api_key: api_key }
- else
- nil
- end
- end
-
- def model_attrs(model_name)
- if model_name == "bge-large-en"
- {
- dimensions: 1024,
- max_sequence_length: 512,
- id: 4,
- pg_function: "<#>",
- tokenizer_class: "DiscourseAi::Tokenizer::BgeLargeEnTokenizer",
- }
- elsif model_name == "bge-m3"
- {
- dimensions: 1024,
- max_sequence_length: 8192,
- id: 8,
- pg_function: "<#>",
- tokenizer_class: "DiscourseAi::Tokenizer::BgeM3Tokenizer",
- }
- elsif model_name == "gemini"
- {
- dimensions: 768,
- max_sequence_length: 1536,
- id: 5,
- pg_function: "<=>",
- tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
- }
- elsif model_name == "multilingual-e5-large"
- {
- dimensions: 1024,
- max_sequence_length: 512,
- id: 3,
- pg_function: "<=>",
- tokenizer_class: "DiscourseAi::Tokenizer::MultilingualE5LargeTokenizer",
- }
- elsif model_name == "text-embedding-3-large"
- {
- dimensions: 2000,
- max_sequence_length: 8191,
- id: 7,
- pg_function: "<=>",
- tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
- provider_params: {
- model_name: "text-embedding-3-large",
- },
- }
- elsif model_name == "text-embedding-3-small"
- {
- dimensions: 1536,
- max_sequence_length: 8191,
- id: 6,
- pg_function: "<=>",
- tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
- provider_params: {
- model_name: "text-embedding-3-small",
- },
- }
- else
- {
- dimensions: 1536,
- max_sequence_length: 8191,
- id: 2,
- pg_function: "<=>",
- tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
- provider_params: {
- model_name: "text-embedding-ada-002",
- },
- }
- end
- end
-
- def persist_config(attrs)
- DB.exec(
- <<~SQL,
- INSERT INTO embedding_definitions (id, display_name, dimensions, max_sequence_length, version, pg_function, provider, tokenizer_class, url, api_key, provider_params, seeded, created_at, updated_at)
- VALUES (:id, :display_name, :dimensions, :max_sequence_length, 1, :pg_function, :provider, :tokenizer_class, :url, :api_key, :provider_params, :seeded, :now, :now)
- SQL
- id: attrs[:id],
- display_name: attrs[:display_name],
- dimensions: attrs[:dimensions],
- max_sequence_length: attrs[:max_sequence_length],
- pg_function: attrs[:pg_function],
- provider: attrs[:provider],
- tokenizer_class: attrs[:tokenizer_class],
- url: attrs[:url],
- api_key: attrs[:api_key],
- provider_params: attrs[:provider_params]&.to_json,
- seeded: !!attrs[:seeded],
- now: Time.zone.now,
- )
-
- # We hardcoded the ID to match with already generated embeddings. Let's restart the seq to avoid conflicts.
- DB.exec(
- "ALTER SEQUENCE embedding_definitions_id_seq RESTART WITH :new_seq",
- new_seq: attrs[:id].to_i + 1,
- )
-
- DB.exec(<<~SQL, new_value: attrs[:id])
- INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
- VALUES ('ai_embeddings_selected_model', 3, :new_value, NOW(), NOW())
- SQL
- end
-end
diff --git a/db/migrate/20250114160417_backfill_topic_embeddings.rb b/db/migrate/20250114160417_backfill_topic_embeddings.rb
deleted file mode 100644
index 3ec94de6..00000000
--- a/db/migrate/20250114160417_backfill_topic_embeddings.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-class BackfillTopicEmbeddings < ActiveRecord::Migration[7.2]
- disable_ddl_transaction!
-
- def up
- if table_exists?(:ai_topic_embeddings)
- loop do
- count = execute(<<~SQL).cmd_tuples
- INSERT INTO ai_topics_embeddings (topic_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT source.*
- FROM (
- SELECT old_table.*
- FROM ai_topic_embeddings old_table
- LEFT JOIN ai_topics_embeddings target ON (
- target.model_id = old_table.model_id AND
- target.strategy_id = old_table.strategy_id AND
- target.topic_id = old_table.topic_id
- )
- WHERE target.topic_id IS NULL
- LIMIT 10000
- ) source
- SQL
-
- break if count == 0
- end
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250114160446_backfill_post_embeddings.rb b/db/migrate/20250114160446_backfill_post_embeddings.rb
deleted file mode 100644
index f314f90d..00000000
--- a/db/migrate/20250114160446_backfill_post_embeddings.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-class BackfillPostEmbeddings < ActiveRecord::Migration[7.2]
- disable_ddl_transaction!
-
- def up
- if table_exists?(:ai_post_embeddings)
- # Copy data from old tables to new tables in batches.
-
- loop do
- count = execute(<<~SQL).cmd_tuples
- INSERT INTO ai_posts_embeddings (post_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT source.*
- FROM (
- SELECT old_table.*
- FROM ai_post_embeddings old_table
- LEFT JOIN ai_posts_embeddings target ON (
- target.model_id = old_table.model_id AND
- target.strategy_id = old_table.strategy_id AND
- target.post_id = old_table.post_id
- )
- WHERE target.post_id IS NULL
- LIMIT 10000
- ) source
- SQL
-
- break if count == 0
- end
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250114160500_backfill_rag_embeddings.rb b/db/migrate/20250114160500_backfill_rag_embeddings.rb
deleted file mode 100644
index 46f73e7a..00000000
--- a/db/migrate/20250114160500_backfill_rag_embeddings.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-class BackfillRagEmbeddings < ActiveRecord::Migration[7.2]
- def up
- if table_exists?(:ai_document_fragment_embeddings)
- not_backfilled =
- DB.query_single("SELECT COUNT(*) FROM ai_document_fragments_embeddings").first.to_i == 0
-
- if not_backfilled
- # Copy data from old tables to new tables
- execute <<~SQL
- INSERT INTO ai_document_fragments_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- SELECT * FROM ai_document_fragment_embeddings;
- SQL
- end
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250115173456_add_highest_target_number_to_ai_summary.rb b/db/migrate/20250115173456_add_highest_target_number_to_ai_summary.rb
deleted file mode 100644
index b998f2c5..00000000
--- a/db/migrate/20250115173456_add_highest_target_number_to_ai_summary.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-class AddHighestTargetNumberToAiSummary < ActiveRecord::Migration[7.2]
- def up
- add_column :ai_summaries, :highest_target_number, :integer, null: false, default: 1
-
- execute <<~SQL
- UPDATE ai_summaries SET highest_target_number = GREATEST(UPPER(content_range) - 1, 1)
- SQL
- end
-
- def down
- drop_column :ai_summaries, :highest_target_number
- end
-end
diff --git a/db/migrate/20250121162520_configurable_embeddings_prefixes.rb b/db/migrate/20250121162520_configurable_embeddings_prefixes.rb
deleted file mode 100644
index 2064ed85..00000000
--- a/db/migrate/20250121162520_configurable_embeddings_prefixes.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-class ConfigurableEmbeddingsPrefixes < ActiveRecord::Migration[7.2]
- def up
- add_column :embedding_definitions, :embed_prompt, :string, null: false, default: ""
- add_column :embedding_definitions, :search_prompt, :string, null: false, default: ""
-
- # 4 is bge-large-en. Default model and the only one using this so far.
- execute <<~SQL
- UPDATE embedding_definitions
- SET search_prompt='Represent this sentence for searching relevant passages:'
- WHERE id = 4
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250122003035_add_duration_to_ai_api_log.rb b/db/migrate/20250122003035_add_duration_to_ai_api_log.rb
deleted file mode 100644
index a8e99b4a..00000000
--- a/db/migrate/20250122003035_add_duration_to_ai_api_log.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class AddDurationToAiApiLog < ActiveRecord::Migration[7.2]
- def change
- add_column :ai_api_audit_logs, :duration_msecs, :integer
- end
-end
diff --git a/db/migrate/20250122131007_matryoshka_dimensions_support.rb b/db/migrate/20250122131007_matryoshka_dimensions_support.rb
deleted file mode 100644
index a8a38174..00000000
--- a/db/migrate/20250122131007_matryoshka_dimensions_support.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-class MatryoshkaDimensionsSupport < ActiveRecord::Migration[7.2]
- def change
- add_column :embedding_definitions, :matryoshka_dimensions, :boolean, null: false, default: false
-
- execute <<~SQL
- UPDATE embedding_definitions
- SET matryoshka_dimensions = TRUE
- WHERE
- provider = 'open_ai' AND
- provider_params IS NOT NULL AND
- (
- (provider_params->>'model_name') = 'text-embedding-3-large' OR
- (provider_params->>'model_name') = 'text-embedding-3-small'
- )
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250125162658_fix_broken_open_ai_embeddings_config.rb b/db/migrate/20250125162658_fix_broken_open_ai_embeddings_config.rb
deleted file mode 100644
index 32b6ab80..00000000
--- a/db/migrate/20250125162658_fix_broken_open_ai_embeddings_config.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-class FixBrokenOpenAiEmbeddingsConfig < ActiveRecord::Migration[7.2]
- def up
- return if fetch_setting("ai_embeddings_selected_model").present?
-
- return if DB.query_single("SELECT COUNT(*) FROM embedding_definitions").first > 0
-
- open_ai_models = %w[text-embedding-3-large text-embedding-3-small text-embedding-ada-002]
- current_model = fetch_setting("ai_embeddings_model")
- return if !open_ai_models.include?(current_model)
-
- endpoint = fetch_setting("ai_openai_embeddings_url") || "https://api.openai.com/v1/embeddings"
- api_key = fetch_setting("ai_openai_api_key")
- return if api_key.blank?
-
- attrs = {
- display_name: current_model,
- url: endpoint,
- api_key: api_key,
- provider: "open_ai",
- }.merge(model_attrs(current_model))
-
- persist_config(attrs)
- end
-
- def fetch_setting(name)
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: name,
- ).first || ENV["DISCOURSE_#{name&.upcase}"]
- end
-
- def model_attrs(model_name)
- if model_name == "text-embedding-3-large"
- {
- dimensions: 2000,
- max_sequence_length: 8191,
- id: 7,
- pg_function: "<=>",
- tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
- matryoshka_dimensions: true,
- provider_params: {
- model_name: "text-embedding-3-large",
- },
- }
- elsif model_name == "text-embedding-3-small"
- {
- dimensions: 1536,
- max_sequence_length: 8191,
- id: 6,
- pg_function: "<=>",
- tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
- provider_params: {
- model_name: "text-embedding-3-small",
- },
- }
- else
- {
- dimensions: 1536,
- max_sequence_length: 8191,
- id: 2,
- pg_function: "<=>",
- tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
- provider_params: {
- model_name: "text-embedding-ada-002",
- },
- }
- end
- end
-
- def persist_config(attrs)
- DB.exec(
- <<~SQL,
- INSERT INTO embedding_definitions (id, display_name, dimensions, max_sequence_length, version, pg_function, provider, tokenizer_class, url, api_key, provider_params, matryoshka_dimensions, created_at, updated_at)
- VALUES (:id, :display_name, :dimensions, :max_sequence_length, 1, :pg_function, :provider, :tokenizer_class, :url, :api_key, :provider_params, :matryoshka_dimensions, :now, :now)
- SQL
- id: attrs[:id],
- display_name: attrs[:display_name],
- dimensions: attrs[:dimensions],
- max_sequence_length: attrs[:max_sequence_length],
- pg_function: attrs[:pg_function],
- provider: attrs[:provider],
- tokenizer_class: attrs[:tokenizer_class],
- url: attrs[:url],
- api_key: attrs[:api_key],
- provider_params: attrs[:provider_params]&.to_json,
- matryoshka_dimensions: !!attrs[:matryoshka_dimensions],
- now: Time.zone.now,
- )
-
- # We hardcoded the ID to match with already generated embeddings. Let's restart the seq to avoid conflicts.
- DB.exec(
- "ALTER SEQUENCE embedding_definitions_id_seq RESTART WITH :new_seq",
- new_seq: attrs[:id].to_i + 1,
- )
-
- DB.exec(<<~SQL, new_value: attrs[:id])
- INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
- VALUES ('ai_embeddings_selected_model', 3, ':new_value', NOW(), NOW())
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250127145305_clean_unused_embedding_search_indexes.rb b/db/migrate/20250127145305_clean_unused_embedding_search_indexes.rb
deleted file mode 100644
index 9c6e0d17..00000000
--- a/db/migrate/20250127145305_clean_unused_embedding_search_indexes.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-class CleanUnusedEmbeddingSearchIndexes < ActiveRecord::Migration[7.2]
- def up
- existing_definitions =
- DB.query("SELECT id, dimensions FROM embedding_definitions WHERE id <= 8")
-
- drop_statements =
- (1..8)
- .reduce([]) do |memo, model_id|
- model = existing_definitions.find { |ed| ed&.id == model_id }
-
- if model.blank? || !correctly_indexed?(model)
- embedding_tables.each do |type|
- memo << "DROP INDEX IF EXISTS ai_#{type}_embeddings_#{model_id}_1_search_bit;"
- end
- end
-
- memo
- end
- .join("\n")
-
- DB.exec(drop_statements) if drop_statements.present?
-
- amend_statements =
- (1..8)
- .reduce([]) do |memo, model_id|
- model = existing_definitions.find { |ed| ed&.id == model_id }
-
- memo << amended_idxs(model) if model.present? && !correctly_indexed?(model)
-
- memo
- end
- .join("\n")
-
- DB.exec(amend_statements) if amend_statements.present?
- end
-
- def embedding_tables
- %w[topics posts document_fragments]
- end
-
- def amended_idxs(model)
- embedding_tables.map { |t| <<~SQL }.join("\n")
- CREATE INDEX IF NOT EXISTS ai_#{t}_embeddings_#{model.id}_1_search_bit ON ai_#{t}_embeddings
- USING hnsw ((binary_quantize(embeddings)::bit(#{model.dimensions})) bit_hamming_ops)
- WHERE model_id = #{model.id} AND strategy_id = 1;
- SQL
- end
-
- def correctly_indexed?(edef)
- seeded_dimensions[edef.id] == edef.dimensions
- end
-
- def seeded_dimensions
- { 1 => 768, 2 => 1536, 3 => 1024, 4 => 1024, 5 => 768, 6 => 1536, 7 => 2000, 8 => 1024 }
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250210024600_add_rag_llm_model.rb b/db/migrate/20250210024600_add_rag_llm_model.rb
deleted file mode 100644
index bcf8ad79..00000000
--- a/db/migrate/20250210024600_add_rag_llm_model.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-class AddRagLlmModel < ActiveRecord::Migration[7.2]
- def change
- add_column :ai_personas, :rag_llm_model_id, :bigint
- add_column :ai_tools, :rag_llm_model_id, :bigint
- end
-end
diff --git a/db/migrate/20250210032345_migrate_persona_to_llm_model_id.rb b/db/migrate/20250210032345_migrate_persona_to_llm_model_id.rb
deleted file mode 100644
index 4c601703..00000000
--- a/db/migrate/20250210032345_migrate_persona_to_llm_model_id.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-class MigratePersonaToLlmModelId < ActiveRecord::Migration[7.2]
- def up
- add_column :ai_personas, :default_llm_id, :bigint
- add_column :ai_personas, :question_consolidator_llm_id, :bigint
- # personas are seeded, we do not mark stuff as readonline
-
- execute <<~SQL
- UPDATE ai_personas
- set
- default_llm_id = (select id from llm_models where ('custom:' || id) = default_llm),
- question_consolidator_llm_id = (select id from llm_models where ('custom:' || id) = question_consolidator_llm)
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250211021037_add_error_to_ai_spam_log.rb b/db/migrate/20250211021037_add_error_to_ai_spam_log.rb
deleted file mode 100644
index f1b5a175..00000000
--- a/db/migrate/20250211021037_add_error_to_ai_spam_log.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class AddErrorToAiSpamLog < ActiveRecord::Migration[7.2]
- def change
- add_column :ai_spam_logs, :error, :string, limit: 3000
- end
-end
diff --git a/db/migrate/20250310172527_add_ai_search_discoveries_to_user_options.rb b/db/migrate/20250310172527_add_ai_search_discoveries_to_user_options.rb
deleted file mode 100644
index 44620c96..00000000
--- a/db/migrate/20250310172527_add_ai_search_discoveries_to_user_options.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class AddAiSearchDiscoveriesToUserOptions < ActiveRecord::Migration[7.2]
- def change
- add_column :user_options, :ai_search_discoveries, :boolean, default: true, null: false
- end
-end
diff --git a/db/migrate/20250407125756_set_correct_default_for_short_summarizer_persona.rb b/db/migrate/20250407125756_set_correct_default_for_short_summarizer_persona.rb
deleted file mode 100644
index 65e77ee6..00000000
--- a/db/migrate/20250407125756_set_correct_default_for_short_summarizer_persona.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-class SetCorrectDefaultForShortSummarizerPersona < ActiveRecord::Migration[7.2]
- def up
- execute <<~SQL
- UPDATE ai_personas
- SET allowed_group_ids = ARRAY[0]
- WHERE id = -12 AND allowed_group_ids = '{}'
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250411121705_add_response_format_json_to_personass.rb b/db/migrate/20250411121705_add_response_format_json_to_personass.rb
deleted file mode 100644
index 2f5a3e10..00000000
--- a/db/migrate/20250411121705_add_response_format_json_to_personass.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class AddResponseFormatJsonToPersonass < ActiveRecord::Migration[7.2]
- def change
- add_column :ai_personas, :response_format, :jsonb
- end
-end
diff --git a/db/migrate/20250416215039_add_cost_metrics_to_llm_model.rb b/db/migrate/20250416215039_add_cost_metrics_to_llm_model.rb
deleted file mode 100644
index 1df3ccac..00000000
--- a/db/migrate/20250416215039_add_cost_metrics_to_llm_model.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class AddCostMetricsToLlmModel < ActiveRecord::Migration[7.2]
- def change
- add_column :llm_models, :input_cost, :float
- add_column :llm_models, :cached_input_cost, :float
- add_column :llm_models, :output_cost, :float
- end
-end
diff --git a/db/migrate/20250417194503_add_max_output_tokens_to_llm_model.rb b/db/migrate/20250417194503_add_max_output_tokens_to_llm_model.rb
deleted file mode 100644
index 0272234a..00000000
--- a/db/migrate/20250417194503_add_max_output_tokens_to_llm_model.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class AddMaxOutputTokensToLlmModel < ActiveRecord::Migration[7.2]
- def change
- add_column :llm_models, :max_output_tokens, :integer
- end
-end
diff --git a/db/migrate/20250424035234_remove_old_settings.rb b/db/migrate/20250424035234_remove_old_settings.rb
deleted file mode 100644
index c97c9ca8..00000000
--- a/db/migrate/20250424035234_remove_old_settings.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-class RemoveOldSettings < ActiveRecord::Migration[7.2]
- def up
- execute <<~SQL
- DELETE FROM site_settings
- WHERE name IN ('ai_bot_enabled_chat_bots')
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250424163718_set_ai_bot_pm_custom_fields.rb b/db/migrate/20250424163718_set_ai_bot_pm_custom_fields.rb
deleted file mode 100644
index 02703c5e..00000000
--- a/db/migrate/20250424163718_set_ai_bot_pm_custom_fields.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-class SetAiBotPmCustomFields < ActiveRecord::Migration[7.2]
- def up
- # Set the topic custom field for past bot PMs:
- # - Created by a "real" user (user_id > 0)
- # - Include exactly 2 participants (creator and 1 bot)
- # - One participant is a bot (ID <= -1200)
-
- execute <<~SQL
- INSERT INTO topic_custom_fields (topic_id, name, value, created_at, updated_at)
- SELECT t.id, 'is_ai_bot_pm', 't', NOW(), NOW()
- FROM topics t
- WHERE t.archetype = 'private_message'
- AND t.user_id > 0 -- Created by a real user
- AND (
- SELECT COUNT(*)
- FROM topic_allowed_users tau
- WHERE tau.topic_id = t.id
- ) = 2 -- Only 2 participants total
- AND (
- SELECT COUNT(*)
- FROM topic_allowed_users tau
- WHERE tau.topic_id = t.id
- AND tau.user_id <= -1200 -- Bot users have IDs <= -1200
- ) = 1 -- One of those participants is a bot
- AND NOT EXISTS (
- SELECT 1
- FROM topic_custom_fields tcf
- WHERE tcf.topic_id = t.id
- AND tcf.name = 'is_ai_bot_pm'
- ) -- Don't duplicate existing custom fields
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250429060311_move_dall_e_url.rb b/db/migrate/20250429060311_move_dall_e_url.rb
deleted file mode 100644
index e818ff3d..00000000
--- a/db/migrate/20250429060311_move_dall_e_url.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-class MoveDallEUrl < ActiveRecord::Migration[7.2]
- def up
- execute <<~SQL
- UPDATE site_settings
- SET name = 'ai_openai_image_generation_url'
- WHERE name = 'ai_openai_dall_e_3_url'
- AND NOT EXISTS (
- SELECT 1
- FROM site_settings
- WHERE name = 'ai_openai_image_generation_url')
- SQL
-
- execute <<~SQL
- DELETE FROM site_settings
- WHERE name = 'ai_openai_dall_e_3_url'
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250501002657_renamed_experimental_ai_bot_setting.rb b/db/migrate/20250501002657_renamed_experimental_ai_bot_setting.rb
deleted file mode 100644
index b272c3b8..00000000
--- a/db/migrate/20250501002657_renamed_experimental_ai_bot_setting.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-class RenamedExperimentalAiBotSetting < ActiveRecord::Migration[7.2]
- def up
- execute "UPDATE site_settings SET name = 'ai_bot_enable_dedicated_ux' WHERE name = 'ai_enable_experimental_bot_ux'"
- end
-
- def down
- execute "UPDATE site_settings SET name = 'ai_enable_experimental_bot_ux' WHERE name = 'ai_bot_enable_dedicated_ux'"
- end
-end
diff --git a/db/migrate/20250508154953_add_examples_to_personas.rb b/db/migrate/20250508154953_add_examples_to_personas.rb
deleted file mode 100644
index 2cf12912..00000000
--- a/db/migrate/20250508154953_add_examples_to_personas.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class AddExamplesToPersonas < ActiveRecord::Migration[7.2]
- def change
- add_column :ai_personas, :examples, :jsonb
- end
-end
diff --git a/db/migrate/20250508182047_create_inferred_concepts_table.rb b/db/migrate/20250508182047_create_inferred_concepts_table.rb
deleted file mode 100644
index 9b612e4d..00000000
--- a/db/migrate/20250508182047_create_inferred_concepts_table.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-class CreateInferredConceptsTable < ActiveRecord::Migration[7.2]
- def change
- create_table :inferred_concepts do |t|
- t.string :name, null: false
- t.timestamps
- end
-
- add_index :inferred_concepts, :name, unique: true
- end
-end
diff --git a/db/migrate/20250508183456_create_inferred_concept_topics.rb b/db/migrate/20250508183456_create_inferred_concept_topics.rb
deleted file mode 100644
index 24beee87..00000000
--- a/db/migrate/20250508183456_create_inferred_concept_topics.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-class CreateInferredConceptTopics < ActiveRecord::Migration[7.0]
- def change
- create_table :inferred_concept_topics, id: false do |t|
- t.bigint :inferred_concept_id
- t.bigint :topic_id
- t.timestamps
- end
-
- add_index :inferred_concept_topics,
- %i[topic_id inferred_concept_id],
- unique: true,
- name: "index_inferred_concept_topics_uniqueness"
-
- add_index :inferred_concept_topics, :inferred_concept_id
- end
-end
diff --git a/db/migrate/20250509000001_create_inferred_concept_posts.rb b/db/migrate/20250509000001_create_inferred_concept_posts.rb
deleted file mode 100644
index bcd04b87..00000000
--- a/db/migrate/20250509000001_create_inferred_concept_posts.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-class CreateInferredConceptPosts < ActiveRecord::Migration[7.0]
- def change
- create_table :inferred_concept_posts, id: false do |t|
- t.bigint :inferred_concept_id
- t.bigint :post_id
- t.timestamps
- end
-
- add_index :inferred_concept_posts,
- %i[post_id inferred_concept_id],
- unique: true,
- name: "index_inferred_concept_posts_uniqueness"
-
- add_index :inferred_concept_posts, :inferred_concept_id
- end
-end
diff --git a/db/migrate/20250607071239_create_ai_artifacts_key_values.rb b/db/migrate/20250607071239_create_ai_artifacts_key_values.rb
deleted file mode 100644
index 49bcd93b..00000000
--- a/db/migrate/20250607071239_create_ai_artifacts_key_values.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-class CreateAiArtifactsKeyValues < ActiveRecord::Migration[7.2]
- def change
- create_table :ai_artifact_key_values do |t|
- t.bigint :ai_artifact_id, null: false
- t.integer :user_id, null: false
- t.string :key, null: false, limit: 50
- t.string :value, null: false, limit: 20_000
- t.boolean :public, null: false, default: false
- t.timestamps
- end
-
- add_index :ai_artifact_key_values,
- %i[ai_artifact_id user_id key],
- unique: true,
- name: "index_ai_artifact_kv_unique"
- end
-end
diff --git a/db/migrate/20250619105705_add_persona_to_ai_moderation_settings.rb b/db/migrate/20250619105705_add_persona_to_ai_moderation_settings.rb
deleted file mode 100644
index e514d400..00000000
--- a/db/migrate/20250619105705_add_persona_to_ai_moderation_settings.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-class AddPersonaToAiModerationSettings < ActiveRecord::Migration[7.2]
- def change
- add_column :ai_moderation_settings, :ai_persona_id, :bigint, null: false, default: -31
- end
-end
diff --git a/db/migrate/20250620073222_specify_rate_frequency_in_backfill_setting.rb b/db/migrate/20250620073222_specify_rate_frequency_in_backfill_setting.rb
deleted file mode 100644
index 6f98377c..00000000
--- a/db/migrate/20250620073222_specify_rate_frequency_in_backfill_setting.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class SpecifyRateFrequencyInBackfillSetting < ActiveRecord::Migration[7.2]
- def up
- execute "UPDATE site_settings SET name = 'ai_translation_backfill_hourly_rate' WHERE name = 'ai_translation_backfill_rate'"
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250702073222_rename_mixtral_tokenizer_to_mistral_tokenizer.rb b/db/migrate/20250702073222_rename_mixtral_tokenizer_to_mistral_tokenizer.rb
deleted file mode 100644
index 260ac898..00000000
--- a/db/migrate/20250702073222_rename_mixtral_tokenizer_to_mistral_tokenizer.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-class RenameMixtralTokenizerToMistralTokenizer < ActiveRecord::Migration[7.2]
- def up
- execute <<~SQL
- UPDATE
- llm_models
- SET
- tokenizer = 'DiscourseAi::Tokenizer::Mistral'
- WHERE
- tokenizer = 'DiscourseAi::Tokenizer::Mixtral'
- SQL
-
- execute <<~SQL
- UPDATE
- embedding_definitions
- SET
- tokenizer_class = 'DiscourseAi::Tokenizer::Mistral'
- WHERE
- tokenizer_class = 'DiscourseAi::Tokenizer::Mixtral'
- SQL
- end
-
- def down
- execute <<~SQL
- UPDATE
- llm_models
- SET
- tokenizer = 'DiscourseAi::Tokenizer::Mixtral'
- WHERE
- tokenizer = 'DiscourseAi::Tokenizer::Mistral'
- SQL
-
- execute <<~SQL
- UPDATE
- embedding_definitions
- SET
- tokenizer_class = 'DiscourseAi::Tokenizer::Mixtral'
- WHERE
- tokenizer_class = 'DiscourseAi::Tokenizer::Mistral'
- SQL
- end
-end
diff --git a/db/migrate/20250715165701_update_open_ai_embeddings_tokenizer.rb b/db/migrate/20250715165701_update_open_ai_embeddings_tokenizer.rb
deleted file mode 100644
index 1d8b3888..00000000
--- a/db/migrate/20250715165701_update_open_ai_embeddings_tokenizer.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-class UpdateOpenAiEmbeddingsTokenizer < ActiveRecord::Migration[7.2]
- def up
- execute <<~SQL
- UPDATE embedding_definitions
- SET tokenizer_class = 'DiscourseAi::Tokenizer::OpenAiCl100kTokenizer'
- WHERE url LIKE '%https://api.openai.com/%' AND tokenizer_class <> 'DiscourseAi::Tokenizer::OpenAiCl100kTokenizer'
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250717075002_set_translation_backfill_max_age.rb b/db/migrate/20250717075002_set_translation_backfill_max_age.rb
deleted file mode 100644
index 3795cb9f..00000000
--- a/db/migrate/20250717075002_set_translation_backfill_max_age.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-class SetTranslationBackfillMaxAge < ActiveRecord::Migration[7.2]
- def up
- execute <<~SQL
- UPDATE site_settings
- SET value = '20000'
- WHERE name = 'ai_translation_backfill_max_age_days'
- AND value::integer > 20000;
- SQL
-
- execute <<~SQL
- UPDATE site_settings
- SET value = '0'
- WHERE name = 'ai_translation_backfill_max_age_days'
- AND value::integer < 0;
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250721192553_enable_ai_if_already_installed.rb b/db/migrate/20250721192553_enable_ai_if_already_installed.rb
deleted file mode 100644
index cdc1d77c..00000000
--- a/db/migrate/20250721192553_enable_ai_if_already_installed.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-class EnableAiIfAlreadyInstalled < ActiveRecord::Migration[7.2]
- def up
- installed_at = DB.query_single(<<~SQL)&.first
- SELECT created_at FROM schema_migration_details WHERE version='20230224165056'
- SQL
-
- if installed_at && installed_at < 1.hour.ago
- # The plugin was installed before we changed it to be disabled-by-default
- # Therefore, if there is no existing database value, enable the plugin
- execute <<~SQL
- INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
- VALUES('discourse_ai_enabled', 5, 't', NOW(), NOW())
- ON CONFLICT (name) DO NOTHING
- SQL
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20250722082515_add_index_to_ai_topics_embeddings.rb b/db/migrate/20250722082515_add_index_to_ai_topics_embeddings.rb
deleted file mode 100644
index b918d654..00000000
--- a/db/migrate/20250722082515_add_index_to_ai_topics_embeddings.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class AddIndexToAiTopicsEmbeddings < ActiveRecord::Migration[7.2]
- def up
- add_index :ai_topics_embeddings, %i[topic_id model_id]
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/post_migrate/20240724174343_migrate_vision_llms.rb b/db/post_migrate/20240724174343_migrate_vision_llms.rb
deleted file mode 100644
index cc2987a9..00000000
--- a/db/post_migrate/20240724174343_migrate_vision_llms.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-class MigrateVisionLlms < ActiveRecord::Migration[7.1]
- def up
- vision_models = %w[
- claude-3-sonnet
- claude-3-opus
- claude-3-haiku
- gpt-4-vision-preview
- gpt-4-turbo
- gpt-4o
- gemini-1.5-pro
- gemini-1.5-flash
- ]
-
- DB.exec(<<~SQL, names: vision_models)
- UPDATE llm_models
- SET vision_enabled = true
- WHERE name IN (:names)
- SQL
-
- current_value =
- DB.query_single(
- "SELECT value FROM site_settings WHERE name = :setting_name",
- setting_name: "ai_helper_image_caption_model",
- ).first
-
- if current_value && current_value != "llava"
- model_name = current_value.split(":").last
- llm_model =
- DB.query_single("SELECT id FROM llm_models WHERE name = :model", model: model_name).first
-
- if llm_model
- DB.exec(<<~SQL, new: "custom:#{llm_model}") if llm_model
- UPDATE site_settings
- SET value = :new
- WHERE name = 'ai_helper_image_caption_model'
- SQL
- end
- end
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/post_migrate/20240729202857_migrate_persona_llm_override.rb b/db/post_migrate/20240729202857_migrate_persona_llm_override.rb
deleted file mode 100644
index 99167c02..00000000
--- a/db/post_migrate/20240729202857_migrate_persona_llm_override.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-class MigratePersonaLlmOverride < ActiveRecord::Migration[7.1]
- def up
- fields_to_update = DB.query(<<~SQL)
- SELECT id, default_llm
- FROM ai_personas
- WHERE default_llm IS NOT NULL
- SQL
-
- return if fields_to_update.empty?
-
- updated_fields =
- fields_to_update
- .map do |field|
- llm_model_id = matching_llm_model(field.default_llm)
-
- "(#{field.id}, 'custom:#{llm_model_id}')" if llm_model_id
- end
- .compact
-
- return if updated_fields.empty?
-
- DB.exec(<<~SQL)
- UPDATE ai_personas
- SET default_llm = new_fields.new_default_llm
- FROM (VALUES #{updated_fields.join(", ")}) AS new_fields(id, new_default_llm)
- WHERE new_fields.id::bigint = ai_personas.id
- SQL
- end
-
- def matching_llm_model(model)
- provider = model.split(":").first
- model_name = model.split(":").last
-
- return if provider == "custom"
-
- DB.query_single(
- "SELECT id FROM llm_models WHERE name = :name AND provider = :provider",
- { name: model_name, provider: provider },
- ).first
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/post_migrate/20240809162837_rename_ai_helper_enabled_setting.rb b/db/post_migrate/20240809162837_rename_ai_helper_enabled_setting.rb
deleted file mode 100644
index 59dadb96..00000000
--- a/db/post_migrate/20240809162837_rename_ai_helper_enabled_setting.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class RenameAiHelperEnabledSetting < ActiveRecord::Migration[7.1]
- def up
- execute "UPDATE site_settings SET name = 'ai_helper_enabled' WHERE name = 'composer_ai_helper_enabled'"
- end
-
- def down
- execute "UPDATE site_settings SET name = 'composer_ai_helper_enabled' WHERE name = 'ai_helper_enabled'"
- end
-end
diff --git a/db/post_migrate/20240809163303_rename_ai_helper_allowed_groups_setting.rb b/db/post_migrate/20240809163303_rename_ai_helper_allowed_groups_setting.rb
deleted file mode 100644
index dc7ae56a..00000000
--- a/db/post_migrate/20240809163303_rename_ai_helper_allowed_groups_setting.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class RenameAiHelperAllowedGroupsSetting < ActiveRecord::Migration[7.1]
- def up
- execute "UPDATE site_settings SET name = 'composer_ai_helper_allowed_groups' WHERE name = 'ai_helper_allowed_groups'"
- end
-
- def down
- execute "UPDATE site_settings SET name = 'ai_helper_allowed_groups' WHERE name = 'composer_ai_helper_allowed_groups'"
- end
-end
diff --git a/db/post_migrate/20240912055831_drop_persona_id_from_rag_document_fragments.rb b/db/post_migrate/20240912055831_drop_persona_id_from_rag_document_fragments.rb
deleted file mode 100644
index 238c2968..00000000
--- a/db/post_migrate/20240912055831_drop_persona_id_from_rag_document_fragments.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-class DropPersonaIdFromRagDocumentFragments < ActiveRecord::Migration[7.1]
- def change
- execute <<~SQL
- UPDATE rag_document_fragments
- SET
- target_type = 'AiPersona',
- target_id = ai_persona_id
- WHERE ai_persona_id IS NOT NULL
- SQL
-
- # unlikely but lets be safe
- execute <<~SQL
- DELETE FROM rag_document_fragments
- WHERE target_id IS NULL OR target_type IS NULL
- SQL
-
- remove_column :rag_document_fragments, :ai_persona_id
- change_column_null :rag_document_fragments, :target_id, false
- change_column_null :rag_document_fragments, :target_type, false
- end
-end
diff --git a/db/post_migrate/20241008055831_drop_old_embeddings_indexes.rb b/db/post_migrate/20241008055831_drop_old_embeddings_indexes.rb
deleted file mode 100644
index 347d2b59..00000000
--- a/db/post_migrate/20241008055831_drop_old_embeddings_indexes.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-class DropOldEmbeddingsIndexes < ActiveRecord::Migration[7.1]
- def up
- execute <<~SQL
- DROP INDEX IF EXISTS ai_topic_embeddings_1_1_search;
- DROP INDEX IF EXISTS ai_topic_embeddings_2_1_search;
- DROP INDEX IF EXISTS ai_topic_embeddings_3_1_search;
- DROP INDEX IF EXISTS ai_topic_embeddings_4_1_search;
- DROP INDEX IF EXISTS ai_topic_embeddings_5_1_search;
- DROP INDEX IF EXISTS ai_topic_embeddings_6_1_search;
- DROP INDEX IF EXISTS ai_topic_embeddings_7_1_search;
- DROP INDEX IF EXISTS ai_topic_embeddings_8_1_search;
-
- DROP INDEX IF EXISTS ai_post_embeddings_1_1_search;
- DROP INDEX IF EXISTS ai_post_embeddings_2_1_search;
- DROP INDEX IF EXISTS ai_post_embeddings_3_1_search;
- DROP INDEX IF EXISTS ai_post_embeddings_4_1_search;
- DROP INDEX IF EXISTS ai_post_embeddings_5_1_search;
- DROP INDEX IF EXISTS ai_post_embeddings_6_1_search;
- DROP INDEX IF EXISTS ai_post_embeddings_7_1_search;
- DROP INDEX IF EXISTS ai_post_embeddings_8_1_search;
-
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_1_1_search;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_2_1_search;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_3_1_search;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_4_1_search;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_5_1_search;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_6_1_search;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_7_1_search;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_8_1_search;
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/post_migrate/20241014041242_ai_persona_post_migrate_drop_cols.rb b/db/post_migrate/20241014041242_ai_persona_post_migrate_drop_cols.rb
deleted file mode 100644
index 02d50537..00000000
--- a/db/post_migrate/20241014041242_ai_persona_post_migrate_drop_cols.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-class AiPersonaPostMigrateDropCols < ActiveRecord::Migration[7.1]
- def change
- remove_columns :ai_personas, :allow_chat
- remove_columns :ai_personas, :mentionable
- end
-end
diff --git a/db/post_migrate/20241031041242_migrate_sentiment_classification_result_format.rb b/db/post_migrate/20241031041242_migrate_sentiment_classification_result_format.rb
deleted file mode 100644
index 6c4d425a..00000000
--- a/db/post_migrate/20241031041242_migrate_sentiment_classification_result_format.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-class MigrateSentimentClassificationResultFormat < ActiveRecord::Migration[7.1]
- def up
- DB.exec(<<~SQL)
- UPDATE classification_results
- SET
- model_used = 'cardiffnlp/twitter-roberta-base-sentiment-latest',
- classification = jsonb_build_object(
- 'neutral', (classification->>'neutral')::float / 100,
- 'negative', (classification->>'negative')::float / 100,
- 'positive', (classification->>'positive')::float / 100
- )
- WHERE model_used = 'sentiment';
-
- UPDATE classification_results
- SET
- model_used = 'j-hartmann/emotion-english-distilroberta-base',
- classification = jsonb_build_object(
- 'sadness', (classification->>'sadness')::float / 100,
- 'surprise', (classification->>'surprise')::float / 100,
- 'fear', (classification->>'fear')::float / 100,
- 'anger', (classification->>'anger')::float / 100,
- 'joy', (classification->>'joy')::float / 100,
- 'disgust', (classification->>'disgust')::float / 100,
- 'neutral', (classification->>'neutral')::float / 100
- )
- WHERE model_used = 'emotion';
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/post_migrate/20241206115958_rebake_shared_ai_conversation_oneboxes.rb b/db/post_migrate/20241206115958_rebake_shared_ai_conversation_oneboxes.rb
deleted file mode 100644
index 73537eb4..00000000
--- a/db/post_migrate/20241206115958_rebake_shared_ai_conversation_oneboxes.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-class RebakeSharedAiConversationOneboxes < ActiveRecord::Migration[7.2]
- def up
- # Safe marking for rebake using raw SQL
- DB.exec(<<~SQL)
- UPDATE posts
- SET baked_version = NULL
- WHERE raw LIKE '%/discourse-ai/ai-bot/shared-ai-conversations/%';
- SQL
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/post_migrate/20250113171444_drop_old_embedding_tables.rb b/db/post_migrate/20250113171444_drop_old_embedding_tables.rb
deleted file mode 100644
index 544dfa4f..00000000
--- a/db/post_migrate/20250113171444_drop_old_embedding_tables.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-class DropOldEmbeddingTables < ActiveRecord::Migration[7.2]
- def up
- # Copy rag embeddings created during deploy.
- # noop. TODO(roman): Will follow-up with a new migration to drop these tables.
- end
-
- def down
- end
-end
diff --git a/db/post_migrate/20250114184356_drop_old_embedding_tables2.rb b/db/post_migrate/20250114184356_drop_old_embedding_tables2.rb
deleted file mode 100644
index 6b873924..00000000
--- a/db/post_migrate/20250114184356_drop_old_embedding_tables2.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-class DropOldEmbeddingTables2 < ActiveRecord::Migration[7.2]
- def up
- if table_exists?(:ai_document_fragment_embeddings)
- # Copy rag embeddings created during deploy.
- execute <<~SQL
- INSERT INTO ai_document_fragments_embeddings (rag_document_fragment_id, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- (
- SELECT old_table.*
- FROM ai_document_fragment_embeddings old_table
- LEFT OUTER JOIN ai_document_fragments_embeddings target ON (
- target.model_id = old_table.model_id AND
- target.strategy_id = old_table.strategy_id AND
- target.rag_document_fragment_id = old_table.rag_document_fragment_id
- )
- WHERE target.rag_document_fragment_id IS NULL
- )
- SQL
- end
-
- execute <<~SQL
- DROP INDEX IF EXISTS ai_topic_embeddings_1_1_search_bit;
- DROP INDEX IF EXISTS ai_topic_embeddings_2_1_search_bit;
- DROP INDEX IF EXISTS ai_topic_embeddings_3_1_search_bit;
- DROP INDEX IF EXISTS ai_topic_embeddings_4_1_search_bit;
- DROP INDEX IF EXISTS ai_topic_embeddings_5_1_search_bit;
- DROP INDEX IF EXISTS ai_topic_embeddings_6_1_search_bit;
- DROP INDEX IF EXISTS ai_topic_embeddings_7_1_search_bit;
- DROP INDEX IF EXISTS ai_topic_embeddings_8_1_search_bit;
- DROP INDEX IF EXISTS ai_post_embeddings_1_1_search_bit;
- DROP INDEX IF EXISTS ai_post_embeddings_2_1_search_bit;
- DROP INDEX IF EXISTS ai_post_embeddings_3_1_search_bit;
- DROP INDEX IF EXISTS ai_post_embeddings_4_1_search_bit;
- DROP INDEX IF EXISTS ai_post_embeddings_5_1_search_bit;
- DROP INDEX IF EXISTS ai_post_embeddings_6_1_search_bit;
- DROP INDEX IF EXISTS ai_post_embeddings_7_1_search_bit;
- DROP INDEX IF EXISTS ai_post_embeddings_8_1_search_bit;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_1_1_search_bit;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_2_1_search_bit;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_3_1_search_bit;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_4_1_search_bit;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_5_1_search_bit;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_6_1_search_bit;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_7_1_search_bit;
- DROP INDEX IF EXISTS ai_document_fragment_embeddings_8_1_search_bit;
- SQL
-
- drop_table :ai_topic_embeddings, if_exists: true
- drop_table :ai_post_embeddings, if_exists: true
- drop_table :ai_document_fragment_embeddings, if_exists: true
- end
-
- def down
- end
-end
diff --git a/db/post_migrate/20250115181147_drop_ai_summaries_content_range.rb b/db/post_migrate/20250115181147_drop_ai_summaries_content_range.rb
deleted file mode 100644
index 846c3e1e..00000000
--- a/db/post_migrate/20250115181147_drop_ai_summaries_content_range.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-class DropAiSummariesContentRange < ActiveRecord::Migration[7.2]
- DROPPED_COLUMNS = { ai_summaries: %i[content_range] }
-
- def up
- DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) }
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/post_migrate/20250210032351_post_migrate_persona_to_llm_model_id.rb b/db/post_migrate/20250210032351_post_migrate_persona_to_llm_model_id.rb
deleted file mode 100644
index 0d161b56..00000000
--- a/db/post_migrate/20250210032351_post_migrate_persona_to_llm_model_id.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-class PostMigratePersonaToLlmModelId < ActiveRecord::Migration[7.2]
- def up
- remove_column :ai_personas, :default_llm
- remove_column :ai_personas, :question_consolidator_llm
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/post_migrate/20250404045050_migrate_users_to_email_group.rb b/db/post_migrate/20250404045050_migrate_users_to_email_group.rb
deleted file mode 100644
index 3711a2d3..00000000
--- a/db/post_migrate/20250404045050_migrate_users_to_email_group.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-class MigrateUsersToEmailGroup < ActiveRecord::Migration[7.2]
- def up
- execute <<~SQL
- UPDATE discourse_automation_fields
- SET component = 'email_group_user'
- WHERE
- component = 'users' AND
- name = 'receivers' AND
- automation_id IN (SELECT id FROM discourse_automation_automations WHERE script = 'llm_report')
- SQL
- end
-
- def down
- execute <<~SQL
- UPDATE discourse_automation_fields
- SET component = 'users'
- WHERE
- component = 'email_group_user' AND
- name = 'receivers' AND
- automation_id IN (SELECT id FROM discourse_automation_automations WHERE script = 'llm_report')
- SQL
- end
-end
diff --git a/discourse_automation/llm_persona_triage.rb b/discourse_automation/llm_persona_triage.rb
deleted file mode 100644
index cbe6c1ae..00000000
--- a/discourse_automation/llm_persona_triage.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-if defined?(DiscourseAutomation)
- DiscourseAutomation::Scriptable.add("llm_persona_triage") do
- version 1
- run_in_background
-
- triggerables %i[post_created_edited]
-
- field :persona,
- component: :choices,
- required: true,
- extra: {
- content: DiscourseAi::Automation.available_persona_choices,
- }
- field :whisper, component: :boolean
- field :silent_mode, component: :boolean
-
- script do |context, fields|
- post = context["post"]
- next if post&.user&.bot?
-
- persona_id = fields.dig("persona", "value")
- whisper = !!fields.dig("whisper", "value")
- silent_mode = !!fields.dig("silent_mode", "value")
-
- begin
- RateLimiter.new(
- Discourse.system_user,
- "llm_persona_triage_#{post.id}",
- SiteSetting.ai_automation_max_triage_per_post_per_minute,
- 1.minute,
- ).performed!
-
- RateLimiter.new(
- Discourse.system_user,
- "llm_persona_triage",
- SiteSetting.ai_automation_max_triage_per_minute,
- 1.minute,
- ).performed!
-
- DiscourseAi::Automation::LlmPersonaTriage.handle(
- post: post,
- persona_id: persona_id,
- whisper: whisper,
- automation: self.automation,
- silent_mode: silent_mode,
- )
- rescue => e
- Discourse.warn_exception(
- e,
- message: "llm_persona_triage: skipped triage on post #{post.id}",
- )
- raise e if Rails.env.tests?
- end
- end
- end
-end
diff --git a/discourse_automation/llm_report.rb b/discourse_automation/llm_report.rb
deleted file mode 100644
index c5e308d8..00000000
--- a/discourse_automation/llm_report.rb
+++ /dev/null
@@ -1,134 +0,0 @@
-# frozen_string_literal: true
-
-if defined?(DiscourseAutomation)
- module DiscourseAutomation::LlmReport
- end
-
- DiscourseAutomation::Scriptable.add("llm_report") do
- version 1
- triggerables %i[recurring]
-
- field :sender, component: :user, required: true
- field :receivers, component: :email_group_user
- field :topic_id, component: :text
- field :title, component: :text
- field :days, component: :text, required: true, default_value: 7
- field :offset, component: :text, required: true, default_value: 0
- field :instructions,
- component: :message,
- required: true,
- default_value: DiscourseAi::Automation::ReportRunner.default_instructions
- field :sample_size, component: :text, required: true, default_value: 100
- field :tokens_per_post, component: :text, required: true, default_value: 150
-
- field :persona_id,
- component: :choices,
- required: true,
- default_value:
- DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::ReportRunner],
- extra: {
- content:
- DiscourseAi::Automation.available_persona_choices(
- require_user: false,
- require_default_llm: false,
- ),
- }
- field :model,
- component: :choices,
- required: true,
- extra: {
- content: DiscourseAi::Automation.available_models,
- }
-
- field :priority_group, component: :group
- field :categories, component: :categories
- field :tags, component: :tags
-
- field :exclude_categories, component: :categories
- field :exclude_tags, component: :tags
-
- field :allow_secure_categories, component: :boolean
-
- field :top_p, component: :text
- field :temperature, component: :text
-
- field :suppress_notifications, component: :boolean
- field :debug_mode, component: :boolean
-
- script do |context, fields, automation|
- begin
- sender = fields.dig("sender", "value")
- receivers = fields.dig("receivers", "value")
- topic_id = fields.dig("topic_id", "value")
- title = fields.dig("title", "value")
- model = fields.dig("model", "value")
- category_ids = fields.dig("categories", "value")
- tags = fields.dig("tags", "value")
- allow_secure_categories = !!fields.dig("allow_secure_categories", "value")
- debug_mode = !!fields.dig("debug_mode", "value")
- sample_size = fields.dig("sample_size", "value")
- instructions = fields.dig("instructions", "value")
- days = fields.dig("days", "value")
- offset = fields.dig("offset", "value").to_i
- priority_group = fields.dig("priority_group", "value")
- tokens_per_post = fields.dig("tokens_per_post", "value")
- persona_id = fields.dig("persona_id", "value")
-
- exclude_category_ids = fields.dig("exclude_categories", "value")
- exclude_tags = fields.dig("exclude_tags", "value")
-
- top_p = fields.dig("top_p", "value")
- if top_p == "" || top_p.nil?
- top_p = nil
- else
- top_p = top_p.to_f
- end
-
- temperature = fields.dig("temperature", "value")
- if temperature == "" || temperature.nil?
- temperature = nil
- else
- temperature = temperature.to_f
- end
-
- # Backwards-compat for scripts created before this field was added.
- if persona_id == "" || persona_id.nil?
- persona_id =
- DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::ReportRunner]
- end
-
- suppress_notifications = !!fields.dig("suppress_notifications", "value")
- DiscourseAi::Automation::ReportRunner.run!(
- sender_username: sender,
- receivers: receivers,
- topic_id: topic_id,
- title: title,
- persona_id: persona_id,
- model: model,
- category_ids: category_ids,
- tags: tags,
- allow_secure_categories: allow_secure_categories,
- debug_mode: debug_mode,
- sample_size: sample_size,
- instructions: instructions,
- days: days,
- offset: offset,
- priority_group_id: priority_group,
- tokens_per_post: tokens_per_post,
- exclude_category_ids: exclude_category_ids,
- exclude_tags: exclude_tags,
- temperature: temperature,
- top_p: top_p,
- suppress_notifications: suppress_notifications,
- automation: self.automation,
- )
- rescue => e
- Discourse.warn_exception e, message: "Error running LLM report!"
- if Rails.env.development?
- p e
- puts e.backtrace
- end
- end
- end
- end
-end
diff --git a/discourse_automation/llm_tool_triage.rb b/discourse_automation/llm_tool_triage.rb
deleted file mode 100644
index 885f81fb..00000000
--- a/discourse_automation/llm_tool_triage.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-# TODO: this is still highly experimental and subject to a lot of change
-# leaving it off in production for now Sam
-if defined?(DiscourseAutomation) && !Rails.env.production?
- DiscourseAutomation::Scriptable.add("llm_tool_triage") do
- version 1
- run_in_background
-
- triggerables %i[post_created_edited]
-
- field :tool,
- component: :choices,
- required: true,
- extra: {
- content: DiscourseAi::Automation.available_custom_tools,
- }
-
- script do |context, fields|
- tool_id = fields["tool"]["value"]
- post = context["post"]
- return if post&.user&.bot?
-
- begin
- RateLimiter.new(
- Discourse.system_user,
- "llm_tool_triage_#{post.id}",
- SiteSetting.ai_automation_max_triage_per_post_per_minute,
- 1.minute,
- ).performed!
-
- RateLimiter.new(
- Discourse.system_user,
- "llm_tool_triage",
- SiteSetting.ai_automation_max_triage_per_minute,
- 1.minute,
- ).performed!
-
- DiscourseAi::Automation::LlmToolTriage.handle(
- post: post,
- tool_id: tool_id,
- automation: self.automation,
- )
- rescue => e
- Discourse.warn_exception(e, message: "llm_tool_triage: skipped triage on post #{post.id}")
- end
- end
- end
-end
diff --git a/discourse_automation/llm_triage.rb b/discourse_automation/llm_triage.rb
deleted file mode 100644
index cda48c84..00000000
--- a/discourse_automation/llm_triage.rb
+++ /dev/null
@@ -1,140 +0,0 @@
-# frozen_string_literal: true
-
-if defined?(DiscourseAutomation)
- DiscourseAutomation::Scriptable.add("llm_triage") do
- version 1
- run_in_background
-
- placeholder :post
-
- triggerables %i[post_created_edited]
-
- # TODO move to triggerables
- field :include_personal_messages, component: :boolean
-
- # Inputs
- field :model,
- component: :choices,
- required: true,
- extra: {
- content: DiscourseAi::Automation.available_models,
- }
- field :system_prompt, component: :message, required: false
- field :search_for_text, component: :text, required: true
- field :max_post_tokens, component: :text
- field :stop_sequences, component: :text_list, required: false
- field :temperature, component: :text
- field :max_output_tokens, component: :text
-
- # Actions
- field :category, component: :category
- field :tags, component: :tags
- field :hide_topic, component: :boolean
- field :flag_post, component: :boolean
- field :flag_type,
- component: :choices,
- required: false,
- extra: {
- content: DiscourseAi::Automation.flag_types,
- },
- default: "review"
- field :canned_reply_user, component: :user
- field :canned_reply, component: :message
- field :reply_persona,
- component: :choices,
- extra: {
- content:
- DiscourseAi::Automation.available_persona_choices(
- require_user: false,
- require_default_llm: true,
- ),
- }
- field :whisper, component: :boolean
-
- script do |context, fields|
- post = context["post"]
- next if post&.user&.bot?
-
- if post.topic.private_message?
- include_personal_messages = fields.dig("include_personal_messages", "value")
- next if !include_personal_messages
- end
-
- canned_reply = fields.dig("canned_reply", "value")
- canned_reply_user = fields.dig("canned_reply_user", "value")
- reply_persona_id = fields.dig("reply_persona", "value")
- whisper = fields.dig("whisper", "value")
-
- # nothing to do if we already replied
- next if post.user.username == canned_reply_user
- next if post.raw.strip == canned_reply.to_s.strip
-
- system_prompt = fields.dig("system_prompt", "value")
- search_for_text = fields.dig("search_for_text", "value")
- model = fields.dig("model", "value")
-
- category_id = fields.dig("category", "value")
- tags = fields.dig("tags", "value")
- hide_topic = fields.dig("hide_topic", "value")
- flag_post = fields.dig("flag_post", "value")
- flag_type = fields.dig("flag_type", "value")
- max_post_tokens = fields.dig("max_post_tokens", "value").to_i
- temperature = fields.dig("temperature", "value")
- if temperature == "" || temperature.nil?
- temperature = nil
- else
- temperature = temperature.to_f
- end
-
- max_output_tokens = fields.dig("max_output_tokens", "value").to_i
- max_output_tokens = nil if max_output_tokens <= 0
-
- max_post_tokens = nil if max_post_tokens <= 0
-
- stop_sequences = fields.dig("stop_sequences", "value")
-
- begin
- RateLimiter.new(
- Discourse.system_user,
- "llm_triage_#{post.id}",
- SiteSetting.ai_automation_max_triage_per_post_per_minute,
- 1.minute,
- ).performed!
-
- RateLimiter.new(
- Discourse.system_user,
- "llm_triage",
- SiteSetting.ai_automation_max_triage_per_minute,
- 1.minute,
- ).performed!
-
- DiscourseAi::Automation::LlmTriage.handle(
- post: post,
- model: model,
- search_for_text: search_for_text,
- system_prompt: system_prompt,
- category_id: category_id,
- tags: tags,
- canned_reply: canned_reply,
- canned_reply_user: canned_reply_user,
- reply_persona_id: reply_persona_id,
- whisper: whisper,
- hide_topic: hide_topic,
- flag_post: flag_post,
- flag_type: flag_type.to_s.to_sym,
- max_post_tokens: max_post_tokens,
- stop_sequences: stop_sequences,
- automation: self.automation,
- temperature: temperature,
- max_output_tokens: max_output_tokens,
- action: context["action"],
- )
- rescue => e
- Discourse.warn_exception(
- e,
- message: "llm_triage: skipped triage on post #{post.id} #{post.url}",
- )
- end
- end
- end
-end
diff --git a/eslint.config.mjs b/eslint.config.mjs
deleted file mode 100644
index e691ec7f..00000000
--- a/eslint.config.mjs
+++ /dev/null
@@ -1,3 +0,0 @@
-import DiscourseRecommended from "@discourse/lint-configs/eslint";
-
-export default [...DiscourseRecommended];
diff --git a/evals/lib/boot.rb b/evals/lib/boot.rb
deleted file mode 100644
index 11120dc5..00000000
--- a/evals/lib/boot.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-# got to ensure evals are here
-# rubocop:disable Discourse/Plugins/NamespaceConstants
-EVAL_PATH = File.join(__dir__, "../cases")
-# rubocop:enable Discourse/Plugins/NamespaceConstants
-#
-if !Dir.exist?(EVAL_PATH)
- puts "Evals are missing, cloning from discourse/discourse-ai-evals"
-
- success =
- system("git clone git@github.com:discourse/discourse-ai-evals.git '#{EVAL_PATH}' 2>/dev/null")
-
- # Fall back to HTTPS if SSH fails
- if !success
- puts "SSH clone failed, falling back to HTTPS..."
- success = system("git clone https://github.com/discourse/discourse-ai-evals.git '#{EVAL_PATH}'")
- end
-
- if success
- puts "Successfully cloned evals repository"
- else
- abort "Failed to clone evals repository"
- end
-end
-
-discourse_path = ENV["DISCOURSE_PATH"] || File.expand_path(File.join(__dir__, "../../../.."))
-# rubocop:disable Discourse/NoChdir
-Dir.chdir(discourse_path)
-# rubocop:enable Discourse/NoChdir
-
-require "#{discourse_path}/config/environment"
-
-ENV["DISCOURSE_AI_NO_DEBUG"] = "1"
-module DiscourseAi::Evals
-end
diff --git a/evals/lib/cli.rb b/evals/lib/cli.rb
deleted file mode 100644
index e28658ea..00000000
--- a/evals/lib/cli.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-require "optparse"
-
-class DiscourseAi::Evals::Cli
- class Options
- attr_accessor :eval_name, :model, :list, :list_models
- def initialize(eval_name: nil, model: nil, list: false, list_models: false)
- @eval_name = eval_name
- @model = model
- @list = list
- @list_models = list_models
- end
- end
-
- def self.parse_options!
- options = Options.new
-
- parser =
- OptionParser.new do |opts|
- opts.banner = "Usage: evals/run [options]"
-
- opts.on("-e", "--eval NAME", "Name of the evaluation to run") do |eval_name|
- options.eval_name = eval_name
- end
-
- opts.on("--list-models", "List models") { |model| options.list_models = true }
-
- opts.on(
- "-m",
- "--model NAME",
- "Model to evaluate (will eval all models if not specified)",
- ) { |model| options.model = model }
-
- opts.on("-l", "--list", "List evals") { |model| options.list = true }
- end
-
- show_help = ARGV.empty?
- parser.parse!
-
- if show_help
- puts parser
- exit 0
- end
-
- options
- end
-end
diff --git a/evals/lib/eval.rb b/evals/lib/eval.rb
deleted file mode 100644
index d322bed8..00000000
--- a/evals/lib/eval.rb
+++ /dev/null
@@ -1,329 +0,0 @@
-#frozen_string_literal: true
-
-class DiscourseAi::Evals::Eval
- attr_reader :type,
- :path,
- :name,
- :description,
- :id,
- :args,
- :vision,
- :expected_output,
- :expected_output_regex,
- :expected_tool_call,
- :judge
-
- class EvalError < StandardError
- attr_reader :context
-
- def initialize(message, context)
- super(message)
- @context = context
- end
- end
-
- def initialize(path:)
- @yaml = YAML.load_file(path).symbolize_keys
- @path = path
- @name = @yaml[:name]
- @id = @yaml[:id]
- @description = @yaml[:description]
- @vision = @yaml[:vision]
- @type = @yaml[:type]
- @expected_output = @yaml[:expected_output]
- @expected_output_regex = @yaml[:expected_output_regex]
- @expected_output_regex =
- Regexp.new(@expected_output_regex, Regexp::MULTILINE) if @expected_output_regex
- @expected_tool_call = @yaml[:expected_tool_call]
- @expected_tool_call.symbolize_keys! if @expected_tool_call
- @judge = @yaml[:judge]
- @judge.symbolize_keys! if @judge
- if @yaml[:args].is_a?(Array)
- @args = @yaml[:args].map(&:symbolize_keys)
- else
- @args = @yaml[:args].symbolize_keys
- @args.each do |key, value|
- if (key.to_s.include?("_path") || key.to_s == "path") && value.is_a?(String)
- @args[key] = File.expand_path(File.join(File.dirname(path), value))
- end
- end
- end
- end
-
- def run(llm:)
- result =
- case type
- when "helper"
- helper(llm, **args)
- when "pdf_to_text"
- pdf_to_text(llm, **args)
- when "image_to_text"
- image_to_text(llm, **args)
- when "prompt"
- DiscourseAi::Evals::PromptEvaluator.new(llm).prompt_call(args)
- when "edit_artifact"
- edit_artifact(llm, **args)
- when "summarization"
- summarization(llm, **args)
- end
-
- classify_results(result)
- rescue EvalError => e
- { result: :fail, message: e.message, context: e.context }
- end
-
- def print
- puts "#{id}: #{description}"
- end
-
- def to_json
- {
- type: @type,
- path: @path,
- name: @name,
- description: @description,
- id: @id,
- args: @args,
- vision: @vision,
- expected_output: @expected_output,
- expected_output_regex: @expected_output_regex,
- }.compact
- end
-
- private
-
- # @param result [String, Array] the result of the eval, either
- # "llm response" or [{ result: "llm response", other_attrs: here }]
- # @return [Array] an array of hashes with the result classified
- # as pass or fail, along with extra attributes
- def classify_results(result)
- if result.is_a?(Array)
- result.each { |r| r.merge!(classify_result_pass_fail(r)) }
- else
- [classify_result_pass_fail(result)]
- end
- end
-
- def classify_result_pass_fail(result)
- if expected_output
- if result == expected_output
- { result: :pass }
- else
- { result: :fail, expected_output: expected_output, actual_output: result }
- end
- elsif expected_output_regex
- if result.to_s.match?(expected_output_regex)
- { result: :pass }
- else
- { result: :fail, expected_output: expected_output_regex, actual_output: result }
- end
- elsif expected_tool_call
- tool_call = result
-
- if result.is_a?(Array)
- tool_call = result.find { |r| r.is_a?(DiscourseAi::Completions::ToolCall) }
- end
- if !tool_call.is_a?(DiscourseAi::Completions::ToolCall) ||
- (tool_call.name != expected_tool_call[:name]) ||
- (tool_call.parameters != expected_tool_call[:params])
- { result: :fail, expected_output: expected_tool_call, actual_output: result }
- else
- { result: :pass }
- end
- elsif judge
- judge_result(result)
- else
- { result: :pass }
- end
- end
-
- def judge_result(result)
- prompt = judge[:prompt].dup
- if result.is_a?(String)
- prompt.sub!("{{output}}", result)
- args.each { |key, value| prompt.sub!("{{#{key}}}", value.to_s) }
- else
- prompt.sub!("{{output}}", result[:result])
- result.each { |key, value| prompt.sub!("{{#{key}}}", value.to_s) }
- end
-
- prompt += <<~SUFFIX
-
- Reply with a rating from 1 to 10, where 10 is perfect and 1 is terrible.
-
- example output:
-
- [RATING]10[/RATING] perfect output
-
- example output:
-
- [RATING]5[/RATING]
-
- the following failed to preserve... etc...
- SUFFIX
-
- judge_llm = DiscourseAi::Evals::Llm.choose(judge[:llm]).first
-
- DiscourseAi::Completions::Prompt.new(
- "You are an expert judge tasked at testing LLM outputs.",
- messages: [{ type: :user, content: prompt }],
- )
-
- result =
- judge_llm.llm_model.to_llm.generate(prompt, user: Discourse.system_user, temperature: 0)
-
- if rating = result.match(%r{\[RATING\](\d+)\[/RATING\]})
- rating = rating[1].to_i
- end
-
- if rating.to_i >= judge[:pass_rating]
- { result: :pass }
- else
- {
- result: :fail,
- message: "LLM Rating below threshold, it was #{rating}, expecting #{judge[:pass_rating]}",
- context: result,
- }
- end
- end
-
- def helper(llm, input:, name:, locale: nil)
- helper = DiscourseAi::AiHelper::Assistant.new(helper_llm: llm.llm_model)
- user = Discourse.system_user
- if locale
- user = User.new
- class << user
- attr_accessor :effective_locale
- end
-
- user.effective_locale = locale
- user.admin = true
- end
- result =
- helper.generate_and_send_prompt(name, input, current_user = user, force_default_locale: false)
-
- result[:suggestions].first
- end
-
- def image_to_text(llm, path:)
- upload =
- UploadCreator.new(File.open(path), File.basename(path)).create_for(Discourse.system_user.id)
-
- text = +""
- DiscourseAi::Utils::ImageToText
- .new(upload: upload, llm_model: llm.llm_model, user: Discourse.system_user)
- .extract_text do |chunk, error|
- text << chunk if chunk
- text << "\n\n" if chunk
- end
- text
- ensure
- upload.destroy if upload
- end
-
- def pdf_to_text(llm, path:)
- upload =
- UploadCreator.new(File.open(path), File.basename(path)).create_for(Discourse.system_user.id)
-
- text = +""
- DiscourseAi::Utils::PdfToText
- .new(upload: upload, user: Discourse.system_user, llm_model: llm.llm_model)
- .extract_text do |chunk|
- text << chunk if chunk
- text << "\n\n" if chunk
- end
-
- text
- ensure
- upload.destroy if upload
- end
-
- def edit_artifact(llm, css_path:, js_path:, html_path:, instructions_path:)
- css = File.read(css_path)
- js = File.read(js_path)
- html = File.read(html_path)
- instructions = File.read(instructions_path)
- artifact =
- AiArtifact.create!(
- css: css,
- js: js,
- html: html,
- user_id: Discourse.system_user.id,
- post_id: 1,
- name: "eval artifact",
- )
-
- post = Post.new(topic_id: 1, id: 1)
- diff =
- DiscourseAi::AiBot::ArtifactUpdateStrategies::Diff.new(
- llm: llm.llm_model.to_llm,
- post: post,
- user: Discourse.system_user,
- artifact: artifact,
- artifact_version: nil,
- instructions: instructions,
- )
- diff.apply
-
- if diff.failed_searches.present?
- puts "Eval Errors encountered"
- p diff.failed_searches
- raise EvalError.new("Failed to apply all changes", diff.failed_searches)
- end
-
- version = artifact.versions.last
- raise EvalError.new("Invalid JS", version.js) if !valid_javascript?(version.js)
-
- output = { css: version.css, js: version.js, html: version.html }
-
- artifact.destroy
- output
- end
-
- def valid_javascript?(str)
- require "open3"
-
- # Create a temporary file with the JavaScript code
- Tempfile.create(%w[test .js]) do |f|
- f.write(str)
- f.flush
-
- File.write("/tmp/test.js", str)
-
- begin
- Discourse::Utils.execute_command(
- "node",
- "--check",
- f.path,
- failure_message: "Invalid JavaScript syntax",
- timeout: 30, # reasonable timeout in seconds
- )
- true
- rescue Discourse::Utils::CommandError
- false
- end
- end
- rescue StandardError
- false
- end
-
- def summarization(llm, input:)
- topic =
- Topic.new(
- category: Category.last,
- title: "Eval topic for topic summarization",
- id: -99,
- user_id: Discourse.system_user.id,
- )
- Post.new(topic: topic, id: -99, user_id: Discourse.system_user.id, raw: input)
-
- strategy =
- DiscourseAi::Summarization::FoldContent.new(
- llm.llm_proxy,
- DiscourseAi::Summarization::Strategies::TopicSummary.new(topic),
- )
-
- summary = DiscourseAi::TopicSummarization.new(strategy, Discourse.system_user).summarize
- summary.summarized_text
- end
-end
diff --git a/evals/lib/llm.rb b/evals/lib/llm.rb
deleted file mode 100644
index bb1d40de..00000000
--- a/evals/lib/llm.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-class DiscourseAi::Evals::Llm
- def self.configs
- return @configs if @configs
-
- yaml_path = File.join(File.dirname(__FILE__), "../../config/eval-llms.yml")
- local_yaml_path = File.join(File.dirname(__FILE__), "../../config/eval-llms.local.yml")
-
- configs = YAML.load_file(yaml_path)["llms"] || {}
- if File.exist?(local_yaml_path)
- local_configs = YAML.load_file(local_yaml_path)["llms"] || {}
- configs = configs.merge(local_configs)
- end
-
- @configs = configs
- end
-
- def self.print
- configs
- .keys
- .map do |config_name|
- begin
- new(config_name)
- rescue StandardError
- nil
- end
- end
- .compact
- .each { |llm| puts "#{llm.config_name}: #{llm.name} (#{llm.provider})" }
- end
-
- def self.choose(config_name)
- return [] unless configs
- if !config_name || !configs[config_name]
- configs
- .keys
- .map do |name|
- begin
- new(name)
- rescue StandardError
- nil
- end
- end
- .compact
- else
- [new(config_name)]
- end
- end
-
- attr_reader :llm_model, :llm_proxy, :config_name
-
- def initialize(config_name)
- config = self.class.configs[config_name].dup
- if config["api_key_env"]
- api_key_env = config.delete("api_key_env")
- unless ENV[api_key_env]
- raise "Missing API key for #{config_name}, should be set via #{api_key_env}"
- end
- config[:api_key] = ENV[api_key_env]
- elsif config["api_key"]
- config[:api_key] = config.delete("api_key")
- else
- raise "No API key or API key env var configured for #{config_name}"
- end
- @llm_model = LlmModel.new(config.symbolize_keys)
- @llm_proxy = DiscourseAi::Completions::Llm.proxy(@llm_model)
- @config_name = config_name
- end
-
- def provider
- @llm_model.provider
- end
-
- def name
- @llm_model.display_name
- end
-
- def vision?
- @llm_model.vision_enabled
- end
-end
diff --git a/evals/lib/prompts/prompt_evaluator.rb b/evals/lib/prompts/prompt_evaluator.rb
deleted file mode 100644
index d526243d..00000000
--- a/evals/lib/prompts/prompt_evaluator.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-class DiscourseAi::Evals::PromptEvaluator
- def initialize(llm)
- @llm = llm.llm_model.to_llm
- end
-
- def prompt_call(args)
- args = [args] if !args.is_a?(Array)
- runner = DiscourseAi::Evals::PromptSingleTestRunner.new(@llm)
-
- with_tests_progress(total: args.size) do |bump_progress|
- args.flat_map do |test|
- bump_progress.call
-
- prompts, messages, followups, output_thinking, stream, temperature, tools =
- symbolize_test_args(test)
-
- prompts.flat_map do |prompt|
- messages.map do |message|
- runner.run_single_test(
- prompt,
- message,
- followups,
- output_thinking,
- stream,
- temperature,
- tools,
- )
- end
- end
- end
- end
- end
-
- private
-
- def symbolize_test_args(args)
- prompts = args[:prompts] || [args[:prompt]]
- messages = args[:messages] || [args[:message]]
- followups = symbolize_followups(args)
- output_thinking = args[:output_thinking] || false
- stream = args[:stream] || false
- temperature = args[:temperature]
- tools = symbolize_tools(args[:tools])
- [prompts, messages, followups, output_thinking, stream, temperature, tools]
- end
-
- def symbolize_followups(args)
- return nil if args[:followups].nil? && args[:followup].nil?
- followups = args[:followups] || [args[:followup]]
- followups.map do |followup|
- followup = followup.dup.symbolize_keys!
- message = followup[:message].dup.symbolize_keys!
- message[:type] = message[:type].to_sym if message[:type]
- followup[:message] = message
- followup
- end
- end
-
- def symbolize_tools(tools)
- return nil if tools.nil?
- tools.map do |tool|
- tool.symbolize_keys!
- tool.merge(
- parameters: tool[:parameters]&.map { |param| param.transform_keys(&:to_sym) },
- ).compact
- end
- end
-
- def with_tests_progress(total:)
- puts ""
- count = 0
- result =
- yield(
- -> do
- count += 1
- print "\rProcessing test #{count}/#{total}"
- end
- )
- print "\r\033[K"
- result
- end
-end
diff --git a/evals/lib/prompts/single_test_runner.rb b/evals/lib/prompts/single_test_runner.rb
deleted file mode 100644
index 6e7c43f8..00000000
--- a/evals/lib/prompts/single_test_runner.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# frozen_string_literal: true
-
-class DiscourseAi::Evals::PromptSingleTestRunner
- def initialize(llm)
- @llm = llm
- end
-
- # Run a single test with a prompt and message, and some model settings
- # @param prompt [String] the prompt to use
- # @param message [String] the message to use
- # @param followups [Array] an array of followups (messages) to run after the initial prompt
- # @param output_thinking [Boolean] whether to output the thinking state of the model
- # @param stream [Boolean] whether to stream the output of the model
- # @param temperature [Float] the temperature to use when generating completions
- # @param tools [Array] an array of tools to use when generating completions
- # @return [Hash] the prompt, message, and result of the test
- def run_single_test(prompt, message, followups, output_thinking, stream, temperature, tools)
- @c_prompt =
- DiscourseAi::Completions::Prompt.new(prompt, messages: [{ type: :user, content: message }])
- @c_prompt.tools = tools if tools
- generate_result(temperature, output_thinking, stream)
-
- if followups
- followups.each do |followup|
- generate_followup(followup, output_thinking, stream, temperature)
- end
- end
-
- { prompt:, message:, result: @result }
- end
-
- private
-
- def generate_followup(followup, output_thinking, stream, temperature)
- @c_prompt.push_model_response(@result)
- followup_message = set_followup_tool(followup)
- @c_prompt.push(**followup_message)
- begin
- generate_result(temperature, output_thinking, stream)
- rescue => e
- # should not happen but it helps debugging...
- puts e
- result = []
- end
- end
-
- def set_followup_tool(followup)
- @c_prompt.tools = followup[:tools] if followup[:tools]
- followup_message = followup[:message]
- %i[id name].each do |key|
- if followup_message[key].is_a?(Array)
- type, inner_key = followup_message[key]
- # this allows us to dynamically set the id or name of the tool call
- prev = @c_prompt.messages.reverse.find { |m| m[:type] == type.to_sym }
- followup_message[key] = prev[inner_key.to_sym] if prev
- end
- end
- followup_message
- end
-
- def generate_result(temperature, output_thinking, stream)
- @result =
- if stream
- stream_result = []
- @llm.generate(
- @c_prompt,
- user: Discourse.system_user,
- temperature:,
- output_thinking:,
- ) { |partial| stream_result << partial }
- stream_result
- else
- @llm.generate(@c_prompt, user: Discourse.system_user, temperature:, output_thinking:)
- end
- end
-end
diff --git a/evals/lib/runner.rb b/evals/lib/runner.rb
deleted file mode 100644
index e72fa1e0..00000000
--- a/evals/lib/runner.rb
+++ /dev/null
@@ -1,194 +0,0 @@
-#frozen_string_literal: true
-
-class DiscourseAi::Evals::Runner
- class StructuredLogger
- def initialize
- @log = []
- @current_step = @log
- end
-
- def log(name, args: nil, start_time: nil, end_time: nil)
- start_time ||= Time.now.utc
- end_time ||= Time.now.utc
- args ||= {}
- object = { name: name, args: args, start_time: start_time, end_time: end_time }
- @current_step << object
- end
-
- def step(name, args: nil)
- start_time = Time.now.utc
- start_step = @current_step
-
- new_step = { type: :step, name: name, args: args || {}, log: [], start_time: start_time }
-
- @current_step << new_step
- @current_step = new_step[:log]
- yield new_step
- @current_step = start_step
- new_step[:end_time] = Time.now.utc
- end
-
- def to_trace_event_json
- trace_events = []
- process_id = 1
- thread_id = 1
-
- to_trace_event(@log, process_id, thread_id, trace_events)
-
- JSON.pretty_generate({ traceEvents: trace_events })
- end
-
- private
-
- def to_trace_event(log_items, pid, tid, trace_events, parent_start_time = nil)
- log_items.each do |item|
- if item.is_a?(Hash) && item[:type] == :step
- trace_events << {
- name: item[:name],
- cat: "default",
- ph: "B", # Begin event
- pid: pid,
- tid: tid,
- args: item[:args],
- ts: timestamp_in_microseconds(item[:start_time]),
- }
-
- to_trace_event(item[:log], pid, tid, trace_events, item[:start_time])
-
- trace_events << {
- name: item[:name],
- cat: "default",
- ph: "E", # End event
- pid: pid,
- tid: tid,
- ts: timestamp_in_microseconds(item[:end_time]),
- }
- else
- trace_events << {
- name: item[:name],
- cat: "default",
- ph: "B",
- pid: pid,
- tid: tid,
- args: item[:args],
- ts: timestamp_in_microseconds(item[:start_time] || parent_start_time || Time.now.utc),
- s: "p", # Scope: process
- }
- trace_events << {
- name: item[:name],
- cat: "default",
- ph: "E",
- pid: pid,
- tid: tid,
- ts: timestamp_in_microseconds(item[:end_time] || Time.now.utc),
- s: "p",
- }
- end
- end
- end
-
- def timestamp_in_microseconds(time)
- (time.to_f * 1_000_000).to_i
- end
- end
-
- attr_reader :llms, :cases
-
- def self.evals_paths
- @eval_paths ||= Dir.glob(File.join(File.join(__dir__, "../cases"), "*/*.yml"))
- end
-
- def self.evals
- @evals ||= evals_paths.map { |path| DiscourseAi::Evals::Eval.new(path: path) }
- end
-
- def self.print
- evals.each(&:print)
- end
-
- def initialize(eval_name:, llms:)
- @llms = llms
- @eval = self.class.evals.find { |c| c.id == eval_name }
-
- if !@eval
- puts "Error: Unknown evaluation '#{eval_name}'"
- exit 1
- end
-
- if @llms.empty?
- puts "Error: Unknown model 'model'"
- exit 1
- end
- end
-
- def run!
- puts "Running evaluation '#{@eval.id}'"
-
- structured_log_filename = "#{@eval.id}-#{Time.now.strftime("%Y%m%d-%H%M%S")}.json"
- log_filename = "#{@eval.id}-#{Time.now.strftime("%Y%m%d-%H%M%S")}.log"
- logs_dir = File.join(__dir__, "../log")
- FileUtils.mkdir_p(logs_dir)
-
- log_path = File.expand_path(File.join(logs_dir, log_filename))
- structured_log_path = File.expand_path(File.join(logs_dir, structured_log_filename))
-
- logger = Logger.new(File.open(log_path, "a"))
- logger.info("Starting evaluation '#{@eval.id}'")
-
- Thread.current[:llm_audit_log] = logger
- structured_logger = Thread.current[:llm_audit_structured_log] = StructuredLogger.new
-
- structured_logger.step("Evaluating #{@eval.id}", args: @eval.to_json) do
- llms.each do |llm|
- if @eval.vision && !llm.vision?
- logger.info("Skipping LLM: #{llm.name} as it does not support vision")
- next
- end
-
- structured_logger.step("Evaluating with LLM: #{llm.name}") do |step|
- logger.info("Evaluating with LLM: #{llm.name}")
- print "#{llm.name}: "
- results = @eval.run(llm: llm)
-
- results.each do |result|
- step[:args] = result
- step[:cname] = result[:result] == :pass ? :good : :bad
-
- if result[:result] == :fail
- puts "Failed 🔴"
- puts "Error: #{result[:message]}" if result[:message]
- # this is deliberate, it creates a lot of noise, but sometimes for debugging it's useful
- #puts "Context: #{result[:context].to_s[0..2000]}" if result[:context]
- if result[:expected_output] && result[:actual_output]
- puts "---- Expected ----\n#{result[:expected_output]}"
- puts "---- Actual ----\n#{result[:actual_output]}"
- end
- logger.error("Evaluation failed with LLM: #{llm.name}")
- logger.error("Error: #{result[:message]}") if result[:message]
- logger.error("Expected: #{result[:expected_output]}") if result[:expected_output]
- logger.error("Actual: #{result[:actual_output]}") if result[:actual_output]
- logger.error("Context: #{result[:context]}") if result[:context]
- elsif result[:result] == :pass
- puts "Passed 🟢"
- logger.info("Evaluation passed with LLM: #{llm.name}")
- else
- STDERR.puts "Error: Unknown result #{eval.inspect}"
- logger.error("Unknown result: #{eval.inspect}")
- end
- end
- end
- end
- end
-
- #structured_logger.save(structured_log_path)
-
- File.write("#{structured_log_path}", structured_logger.to_trace_event_json)
-
- puts
- puts "Log file: #{log_path}"
- puts "Structured log file (ui.perfetto.dev): #{structured_log_path}"
-
- # temp code
- # puts File.read(structured_log_path)
- end
-end
diff --git a/evals/run b/evals/run
deleted file mode 100755
index 8aa6c4ba..00000000
--- a/evals/run
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require_relative "lib/boot"
-require_relative "lib/llm"
-require_relative "lib/cli"
-require_relative "lib/runner"
-require_relative "lib/eval"
-require_relative "lib/prompts/prompt_evaluator"
-require_relative "lib/prompts/single_test_runner"
-
-options = DiscourseAi::Evals::Cli.parse_options!
-
-if options.list
- DiscourseAi::Evals::Runner.print
- exit 0
-end
-
-if options.list_models
- DiscourseAi::Evals::Llm.print
- exit 0
-end
-
-DiscourseAi::Evals::Runner.new(
- eval_name: options.eval_name,
- llms: DiscourseAi::Evals::Llm.choose(options.model),
-).run!
diff --git a/lib/ai_bot/chat_streamer.rb b/lib/ai_bot/chat_streamer.rb
deleted file mode 100644
index 06357e0e..00000000
--- a/lib/ai_bot/chat_streamer.rb
+++ /dev/null
@@ -1,129 +0,0 @@
-# frozen_string_literal: true
-#
-# Chat streaming APIs are a bit slow, this ensures we properly buffer results
-# and stream as quickly as possible.
-
-module DiscourseAi
- module AiBot
- class ChatStreamer
- attr_reader :reply,
- :guardian,
- :thread_id,
- :force_thread,
- :in_reply_to_id,
- :channel,
- :cancel_manager
-
- def initialize(
- message:,
- channel:,
- guardian:,
- thread_id:,
- in_reply_to_id:,
- force_thread:,
- cancel_manager: nil
- )
- @message = message
- @channel = channel
- @guardian = guardian
- @thread_id = thread_id
- @force_thread = force_thread
- @in_reply_to_id = in_reply_to_id
-
- @queue = Queue.new
-
- db = RailsMultisite::ConnectionManagement.current_db
- @worker_thread =
- Thread.new { RailsMultisite::ConnectionManagement.with_connection(db) { run } }
-
- @client_id =
- ChatSDK::Channel.start_reply(
- channel_id: message.chat_channel_id,
- guardian: guardian,
- thread_id: thread_id,
- )
-
- @cancel_manager = cancel_manager
- end
-
- def <<(partial)
- return if partial.to_s.empty?
- # we throw away leading spaces prior to message creation for now
- # by design
- return if partial.to_s.blank? && !@reply
-
- if @client_id
- ChatSDK::Channel.stop_reply(
- channel_id: @message.chat_channel_id,
- client_id: @client_id,
- guardian: @guardian,
- thread_id: @thread_id,
- )
- @client_id = nil
- end
-
- if @reply
- @queue << partial
- else
- create_reply(partial)
- end
- end
-
- def create_reply(message)
- @reply =
- ChatSDK::Message.create(
- raw: message,
- channel_id: channel.id,
- thread_id: thread_id,
- guardian: guardian,
- force_thread: force_thread,
- in_reply_to_id: in_reply_to_id,
- enforce_membership: !channel.direct_message_channel?,
- )
-
- ChatSDK::Message.start_stream(message_id: @reply.id, guardian: @guardian)
-
- if trailing = message.scan(/\s*\z/).first
- @queue << trailing
- end
- end
-
- def done
- @queue << :done
- @worker_thread.join
- ChatSDK::Message.stop_stream(message_id: @reply.id, guardian: @guardian) if @reply
- @reply
- end
-
- private
-
- def run
- done = false
- while !done
- buffer = +""
- popped = @queue.pop
- break if popped == :done
-
- buffer << popped
-
- begin
- while true
- popped = @queue.pop(true)
- if popped == :done
- done = true
- break
- end
- buffer << popped
- end
- rescue ThreadError
- end
-
- streaming = ChatSDK::Message.stream(message_id: reply.id, raw: buffer, guardian: guardian)
- if !streaming
- @cancel_manager.cancel! if @cancel_manager
- end
- end
- end
- end
- end
-end
diff --git a/lib/ai_bot/entry_point.rb b/lib/ai_bot/entry_point.rb
deleted file mode 100644
index 7bcdc646..00000000
--- a/lib/ai_bot/entry_point.rb
+++ /dev/null
@@ -1,294 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiBot
- USER_AGENT = "Discourse AI Bot 1.0 (https://www.discourse.org)"
- TOPIC_AI_BOT_PM_FIELD = "is_ai_bot_pm"
- POST_AI_LLM_NAME_FIELD = "ai_llm_name"
-
- class EntryPoint
- Bot = Struct.new(:id, :name, :llm)
-
- def self.all_bot_ids
- AiPersona
- .persona_users
- .map { |persona| persona[:user_id] }
- .concat(LlmModel.where(enabled_chat_bot: true).pluck(:user_id))
- end
-
- def self.find_participant_in(participant_ids)
- model = LlmModel.includes(:user).where(user_id: participant_ids).last
- return if model.nil?
-
- bot_user = model.user
-
- Bot.new(bot_user.id, bot_user.username_lower, model.name)
- end
-
- def self.find_user_from_model(model_name)
- # Hack(Roman): Added this because Command R Plus had a different in the bot settings.
- # Will eventually ammend it with a data migration.
- name = model_name
- name = "command-r-plus" if name == "cohere-command-r-plus"
-
- LlmModel.joins(:user).where(name: name).last&.user
- end
-
- def self.enabled_user_ids_and_models_map
- DB.query_hash(<<~SQL)
- SELECT users.username AS username, users.id AS id, llms.name AS model_name, llms.display_name AS display_name
- FROM llm_models llms
- INNER JOIN users ON llms.user_id = users.id
- WHERE llms.enabled_chat_bot
- SQL
- end
-
- # Most errors are simply "not_allowed"
- # we do not want to reveal information about this system
- # the 2 exceptions are "other_people_in_pm" and "other_content_in_pm"
- # in both cases you have access to the PM so we are not revealing anything
- def self.ai_share_error(topic, guardian)
- return nil if guardian.can_share_ai_bot_conversation?(topic)
-
- return :not_allowed if !guardian.can_see?(topic)
-
- # other people in PM
- if topic.topic_allowed_users.where("user_id > 0 and user_id <> ?", guardian.user.id).exists?
- return :other_people_in_pm
- end
-
- # other content in PM
- if topic.posts.where("user_id > 0 and user_id <> ?", guardian.user.id).exists?
- return :other_content_in_pm
- end
-
- :not_allowed
- end
-
- def inject_into(plugin)
- # Long term we need a better API here
- # we only want to load this custom field for bots
- TopicView.default_post_custom_fields << POST_AI_LLM_NAME_FIELD
-
- plugin.register_topic_custom_field_type(TOPIC_AI_BOT_PM_FIELD, :string)
-
- plugin.on(:topic_created) do |topic|
- next if !topic.private_message?
- creator = topic.user
-
- # Only process if creator is not a bot or system user
- next if DiscourseAi::AiBot::Playground.is_bot_user_id?(creator.id)
-
- # Get all bot user IDs defined by the discourse-ai plugin
- bot_ids = DiscourseAi::AiBot::EntryPoint.all_bot_ids
-
- # Check if the only recipients are bots
- recipients = topic.topic_allowed_users.pluck(:user_id)
-
- # Remove creator from recipients for checking
- recipients -= [creator.id]
-
- # If all remaining recipients are AI bots and there's exactly one recipient
- if recipients.length == 1 && (recipients - bot_ids).empty?
- # The only recipient is an AI bot - add the custom field to the topic
- topic.custom_fields[TOPIC_AI_BOT_PM_FIELD] = true
-
- # Save the custom fields
- topic.save_custom_fields
- end
- end
-
- plugin.register_modifier(:chat_allowed_bot_user_ids) do |user_ids, guardian|
- if guardian.user
- allowed_chat =
- AiPersona.allowed_modalities(
- user: guardian.user,
- allow_chat_direct_messages: true,
- allow_chat_channel_mentions: true,
- )
- allowed_bot_ids = allowed_chat.map { |info| info[:user_id] }
- user_ids.concat(allowed_bot_ids)
- end
- user_ids
- end
-
- plugin.on(:site_setting_changed) do |name, _old_value, _new_value|
- if name == :ai_bot_enabled || name == :discourse_ai_enabled
- DiscourseAi::AiBot::SiteSettingsExtension.enable_or_disable_ai_bots
- end
- end
-
- Oneboxer.register_local_handler(
- "discourse_ai/ai_bot/shared_ai_conversations",
- ) do |url, route|
- if route[:action] == "show" && share_key = route[:share_key]
- if conversation = SharedAiConversation.find_by(share_key: share_key)
- conversation.onebox
- end
- end
- end
-
- plugin.on(:reduce_excerpt) do |doc, options|
- doc.css("details").remove if options && options[:strip_details]
- end
-
- plugin.register_seedfu_fixtures(
- Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "ai_bot"),
- )
-
- plugin.add_to_serializer(
- :topic_view,
- :is_bot_pm,
- include_condition: -> do
- object.topic && object.personal_message &&
- object.topic.custom_fields[TOPIC_AI_BOT_PM_FIELD]
- end,
- ) { true }
-
- plugin.add_to_serializer(
- :post,
- :llm_name,
- include_condition: -> do
- object&.topic&.private_message? && object.custom_fields[POST_AI_LLM_NAME_FIELD]
- end,
- ) { object.custom_fields[POST_AI_LLM_NAME_FIELD] }
-
- plugin.add_to_serializer(
- :current_user,
- :ai_enabled_personas,
- include_condition: -> { scope.authenticated? },
- ) do
- DiscourseAi::Personas::Persona
- .all(user: scope.user)
- .map do |persona|
- {
- id: persona.id,
- name: persona.name,
- description: persona.description,
- force_default_llm: persona.force_default_llm,
- username: persona.username,
- }
- end
- end
-
- plugin.add_to_serializer(
- :current_user,
- :can_debug_ai_bot_conversations,
- include_condition: -> do
- SiteSetting.ai_bot_enabled && scope.authenticated? &&
- SiteSetting.ai_bot_debugging_allowed_groups.present? &&
- scope.user.in_any_groups?(SiteSetting.ai_bot_debugging_allowed_groups_map)
- end,
- ) { true }
-
- plugin.add_to_serializer(
- :current_user,
- :ai_enabled_chat_bots,
- include_condition: -> do
- SiteSetting.ai_bot_enabled && scope.authenticated? &&
- scope.user.in_any_groups?(SiteSetting.ai_bot_allowed_groups_map)
- end,
- ) do
- bots_map = ::DiscourseAi::AiBot::EntryPoint.enabled_user_ids_and_models_map
-
- persona_users = AiPersona.persona_users(user: scope.user)
- if persona_users.present?
- persona_users.filter! { |persona_user| persona_user[:username].present? }
-
- bots_map.concat(
- persona_users.map do |persona_user|
- {
- "id" => persona_user[:user_id],
- "username" => persona_user[:username],
- "has_default_llm" => persona_user[:default_llm_id].present?,
- "force_default_llm" => persona_user[:force_default_llm],
- "is_persona" => true,
- }
- end,
- )
- end
-
- bots_map
- end
-
- plugin.add_to_serializer(:current_user, :can_share_ai_bot_conversations) do
- scope.user.in_any_groups?(SiteSetting.ai_bot_public_sharing_allowed_groups_map)
- end
-
- plugin.add_to_serializer(
- :current_user,
- :can_use_ai_bot_discover_persona,
- include_condition: -> do
- SiteSetting.ai_bot_enabled && scope.authenticated? &&
- SiteSetting.ai_bot_discover_persona.present?
- end,
- ) do
- persona_allowed_groups =
- AiPersona.find_by(id: SiteSetting.ai_bot_discover_persona)&.allowed_group_ids.to_a
-
- scope.user.in_any_groups?(persona_allowed_groups)
- end
-
- UserUpdater::OPTION_ATTR.push(:ai_search_discoveries)
- plugin.add_to_serializer(
- :user_option,
- :ai_search_discoveries,
- include_condition: -> do
- SiteSetting.ai_bot_enabled && SiteSetting.ai_bot_discover_persona.present? &&
- scope.authenticated?
- end,
- ) { object.ai_search_discoveries }
-
- plugin.add_to_serializer(
- :current_user_option,
- :ai_search_discoveries,
- include_condition: -> do
- SiteSetting.ai_bot_enabled && SiteSetting.ai_bot_discover_persona.present? &&
- scope.authenticated?
- end,
- ) { object.ai_search_discoveries }
-
- plugin.add_to_serializer(
- :topic_view,
- :ai_persona_name,
- include_condition: -> { SiteSetting.ai_bot_enabled && object.topic.private_message? },
- ) do
- id = topic.custom_fields["ai_persona_id"]
- name = DiscourseAi::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
- name || topic.custom_fields["ai_persona"]
- end
-
- plugin.on(:post_created) { |post| DiscourseAi::AiBot::Playground.schedule_reply(post) }
-
- plugin.on(:chat_message_created) do |chat_message, channel, user, context|
- DiscourseAi::AiBot::Playground.schedule_chat_reply(chat_message, channel, user, context)
- end
-
- if plugin.respond_to?(:register_editable_topic_custom_field)
- plugin.register_editable_topic_custom_field(:ai_persona_id)
- end
-
- plugin.add_api_key_scope(
- :discourse_ai,
- { stream_completion: { actions: %w[discourse_ai/admin/ai_personas#stream_reply] } },
- )
-
- plugin.on(:site_setting_changed) do |name, old_value, new_value|
- if name == :ai_embeddings_selected_model && DiscourseAi::Embeddings.enabled? &&
- new_value != old_value
- RagDocumentFragment.delete_all
- UploadReference
- .where(target: AiPersona.all)
- .each do |ref|
- Jobs.enqueue(
- :digest_rag_upload,
- ai_persona_id: ref.target_id,
- upload_id: ref.upload_id,
- )
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/ai_bot/playground.rb b/lib/ai_bot/playground.rb
deleted file mode 100644
index 07c4984b..00000000
--- a/lib/ai_bot/playground.rb
+++ /dev/null
@@ -1,661 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiBot
- class Playground
- BYPASS_AI_REPLY_CUSTOM_FIELD = "discourse_ai_bypass_ai_reply"
- BOT_USER_PREF_ID_CUSTOM_FIELD = "discourse_ai_bot_user_pref_id"
- # 10 minutes is enough for vast majority of cases
- # there is a small chance that some reasoning models may take longer
- MAX_STREAM_DELAY_SECONDS = 600
-
- attr_reader :bot
-
- # An abstraction to manage the bot and topic interactions.
- # The bot will take care of completions while this class updates the topic title
- # and stream replies.
-
- def self.find_chat_persona(message, channel, user)
- if channel.direct_message_channel?
- AiPersona
- .allowed_modalities(allow_chat_direct_messages: true)
- .find do |p|
- p[:user_id].in?(channel.allowed_user_ids) && (user.group_ids & p[:allowed_group_ids])
- end
- else
- # let's defer on the parse if there is no @ in the message
- if message.message.include?("@")
- mentions = message.parsed_mentions.parsed_direct_mentions
- if mentions.present?
- AiPersona
- .allowed_modalities(allow_chat_channel_mentions: true)
- .find { |p| p[:username].in?(mentions) && (user.group_ids & p[:allowed_group_ids]) }
- end
- end
- end
- end
-
- def self.schedule_chat_reply(message, channel, user, context)
- return if !SiteSetting.ai_bot_enabled
-
- all_chat =
- AiPersona.allowed_modalities(
- allow_chat_channel_mentions: true,
- allow_chat_direct_messages: true,
- )
- return if all_chat.blank?
- return if all_chat.any? { |m| m[:user_id] == user.id }
-
- persona = find_chat_persona(message, channel, user)
- return if !persona
-
- post_ids = nil
- post_ids = context.dig(:context, :post_ids) if context.is_a?(Hash)
-
- ::Jobs.enqueue(
- :create_ai_chat_reply,
- channel_id: channel.id,
- message_id: message.id,
- persona_id: persona[:id],
- context_post_ids: post_ids,
- )
- end
-
- def self.is_bot_user_id?(user_id)
- # this will catch everything and avoid any feedback loops
- # we could get feedback loops between say discobot and ai-bot or third party plugins
- # and bots
- user_id.to_i <= 0
- end
-
- def self.get_bot_user(post:, all_llm_users:, mentionables:)
- bot_user = nil
- if post.topic.private_message?
- # this ensures that we reply using the correct llm
- # 1. if we have a preferred llm user we use that
- # 2. if we don't just take first topic allowed user
- # 3. if we don't have that we take the first mentionable
- bot_user = nil
- if preferred_user =
- all_llm_users.find { |id, username|
- id == post.topic.custom_fields[BOT_USER_PREF_ID_CUSTOM_FIELD].to_i
- }
- bot_user = User.find_by(id: preferred_user[0])
- end
- bot_user ||=
- post.topic.topic_allowed_users.where(user_id: all_llm_users.map(&:first)).first&.user
- bot_user ||=
- post
- .topic
- .topic_allowed_users
- .where(user_id: mentionables.map { |m| m[:user_id] })
- .first
- &.user
- end
- bot_user
- end
-
- def self.schedule_reply(post)
- return if is_bot_user_id?(post.user_id)
- mentionables = nil
-
- if post.topic.private_message?
- mentionables =
- AiPersona.allowed_modalities(user: post.user, allow_personal_messages: true)
- else
- mentionables = AiPersona.allowed_modalities(user: post.user, allow_topic_mentions: true)
- end
-
- mentioned = nil
-
- all_llm_users =
- LlmModel
- .where(enabled_chat_bot: true)
- .joins(:user)
- .pluck("users.id", "users.username_lower")
-
- bot_user =
- get_bot_user(post: post, all_llm_users: all_llm_users, mentionables: mentionables)
-
- mentions = nil
- if mentionables.present? || (bot_user && post.topic.private_message?)
- mentions = post.mentions.map(&:downcase)
-
- # in case we are replying to a post by a bot
- if post.reply_to_post_number && post.reply_to_post&.user
- mentions << post.reply_to_post.user.username_lower
- end
- end
-
- if mentionables.present?
- mentioned = mentionables.find { |mentionable| mentions.include?(mentionable[:username]) }
-
- # direct PM to mentionable
- if !mentioned && bot_user
- mentioned = mentionables.find { |mentionable| bot_user.id == mentionable[:user_id] }
- end
-
- # public topic so we need to use the persona user
- bot_user ||= User.find_by(id: mentioned[:user_id]) if mentioned
- end
-
- if !mentioned && bot_user && post.reply_to_post_number && !post.reply_to_post.user&.bot?
- # replying to a non-bot user
- return
- end
-
- if bot_user
- topic_persona_id = post.topic.custom_fields["ai_persona_id"]
- topic_persona_id = topic_persona_id.to_i if topic_persona_id.present?
-
- persona_id = mentioned&.dig(:id) || topic_persona_id
-
- persona = nil
-
- if persona_id
- persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
- end
-
- if !persona && persona_name = post.topic.custom_fields["ai_persona"]
- persona = DiscourseAi::Personas::Persona.find_by(user: post.user, name: persona_name)
- end
-
- # edge case, llm was mentioned in an ai persona conversation
- if persona_id == topic_persona_id && post.topic.private_message? && persona &&
- all_llm_users.present?
- if !persona.force_default_llm && mentions.present?
- mentioned_llm_user_id, _ =
- all_llm_users.find { |id, username| mentions.include?(username) }
-
- if mentioned_llm_user_id
- bot_user = User.find_by(id: mentioned_llm_user_id) || bot_user
- end
- end
- end
-
- persona ||= DiscourseAi::Personas::General
-
- bot_user = User.find(persona.user_id) if persona && persona.force_default_llm
-
- bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
- new(bot).update_playground_with(post)
- end
- end
-
- def self.reply_to_post(
- post:,
- user: nil,
- persona_id: nil,
- whisper: nil,
- add_user_to_pm: false,
- stream_reply: false,
- auto_set_title: false,
- silent_mode: false,
- feature_name: nil
- )
- ai_persona = AiPersona.find_by(id: persona_id)
- raise Discourse::InvalidParameters.new(:persona_id) if !ai_persona
- persona_class = ai_persona.class_instance
- persona = persona_class.new
-
- bot_user = user || ai_persona.user
- raise Discourse::InvalidParameters.new(:user) if bot_user.nil?
- bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona)
- playground = new(bot)
-
- playground.reply_to(
- post,
- whisper: whisper,
- context_style: :topic,
- add_user_to_pm: add_user_to_pm,
- stream_reply: stream_reply,
- auto_set_title: auto_set_title,
- silent_mode: silent_mode,
- feature_name: feature_name,
- )
- rescue => e
- if Rails.env.test?
- p e
- puts e.backtrace[0..10]
- else
- raise e
- end
- end
-
- def initialize(bot)
- @bot = bot
- end
-
- def update_playground_with(post)
- schedule_bot_reply(post) if can_attach?(post)
- end
-
- def title_playground(post, user)
- messages =
- DiscourseAi::Completions::PromptMessagesBuilder.messages_from_post(
- post,
- max_posts: 5,
- bot_usernames: available_bot_usernames,
- include_uploads: bot.persona.class.vision_enabled,
- )
-
- # conversation context may contain tool calls, and confusing user names
- # clean it up
- conversation = +""
- messages.each do |context|
- if context[:type] == :user
- conversation << "User said:\n#{context[:content]}\n\n"
- elsif context[:type] == :model
- conversation << "Model said:\n#{context[:content]}\n\n"
- end
- end
-
- system_insts = <<~TEXT.strip
- You are titlebot. Given a conversation, you will suggest a title.
-
- - You will never respond with anything but the suggested title.
- - You will always match the conversation language in your title suggestion.
- - Title will capture the essence of the conversation.
- TEXT
-
- instruction = <<~TEXT.strip
- Given the following conversation:
-
- {{{
- #{conversation}
- }}}
-
- Reply only with a title that is 7 words or less.
- TEXT
-
- title_prompt =
- DiscourseAi::Completions::Prompt.new(
- system_insts,
- messages: [type: :user, content: instruction],
- topic_id: post.topic_id,
- )
-
- new_title =
- bot
- .llm
- .generate(title_prompt, user: user, feature_name: "bot_title")
- .strip
- .split("\n")
- .last
-
- PostRevisor.new(post.topic.first_post, post.topic).revise!(
- bot.bot_user,
- title: new_title.sub(/\A"/, "").sub(/"\Z/, ""),
- )
-
- allowed_users = post.topic.topic_allowed_users.pluck(:user_id)
- MessageBus.publish(
- "/discourse-ai/ai-bot/topic/#{post.topic.id}",
- { title: post.topic.title },
- user_ids: allowed_users,
- )
- end
-
- def reply_to_chat_message(message, channel, context_post_ids)
- persona_user = User.find(bot.persona.class.user_id)
-
- participants = channel.user_chat_channel_memberships.map { |m| m.user.username }
-
- context_post_ids = nil if !channel.direct_message_channel?
-
- max_chat_messages = 40
- if bot.persona.class.respond_to?(:max_context_posts)
- max_chat_messages = bot.persona.class.max_context_posts || 40
- end
-
- if !channel.direct_message_channel?
- # we are interacting via mentions ... strip mention
- instruction_message = message.message.gsub(/@#{bot.bot_user.username}/i, "").strip
- end
-
- context =
- DiscourseAi::Personas::BotContext.new(
- participants: participants,
- message_id: message.id,
- channel_id: channel.id,
- context_post_ids: context_post_ids,
- messages:
- DiscourseAi::Completions::PromptMessagesBuilder.messages_from_chat(
- message,
- channel: channel,
- context_post_ids: context_post_ids,
- include_uploads: bot.persona.class.vision_enabled,
- max_messages: max_chat_messages,
- bot_user_ids: available_bot_user_ids,
- instruction_message: instruction_message,
- ),
- user: message.user,
- skip_tool_details: true,
- cancel_manager: DiscourseAi::Completions::CancelManager.new,
- )
-
- reply = nil
- guardian = Guardian.new(persona_user)
-
- force_thread = message.thread_id.nil? && channel.direct_message_channel?
- in_reply_to_id = channel.direct_message_channel? ? message.id : nil
-
- streamer =
- ChatStreamer.new(
- message: message,
- channel: channel,
- guardian: guardian,
- thread_id: message.thread_id,
- in_reply_to_id: in_reply_to_id,
- force_thread: force_thread,
- cancel_manager: context.cancel_manager,
- )
-
- new_prompts =
- bot.reply(context) do |partial, placeholder, type|
- # no support for tools or thinking by design
- next if type == :thinking || type == :tool_details || type == :partial_tool
- streamer << partial
- end
-
- reply = streamer.reply
- if new_prompts.length > 1 && reply
- ChatMessageCustomPrompt.create!(message_id: reply.id, custom_prompt: new_prompts)
- end
-
- if streamer
- streamer.done
- streamer = nil
- end
-
- reply
- ensure
- streamer.done if streamer
- end
-
- def reply_to(
- post,
- custom_instructions: nil,
- whisper: nil,
- context_style: nil,
- add_user_to_pm: true,
- stream_reply: nil,
- auto_set_title: true,
- silent_mode: false,
- feature_name: nil,
- cancel_manager: nil,
- &blk
- )
- # this is a multithreading issue
- # post custom prompt is needed and it may not
- # be properly loaded, ensure it is loaded
- PostCustomPrompt.none
-
- if silent_mode
- auto_set_title = false
- stream_reply = false
- end
-
- reply = +""
- post_streamer = nil
-
- post_type =
- (
- if (whisper || post.post_type == Post.types[:whisper])
- Post.types[:whisper]
- else
- Post.types[:regular]
- end
- )
-
- # safeguard
- max_context_posts = 40
- if bot.persona.class.respond_to?(:max_context_posts)
- max_context_posts = bot.persona.class.max_context_posts || 40
- end
-
- context =
- DiscourseAi::Personas::BotContext.new(
- post: post,
- custom_instructions: custom_instructions,
- feature_name: feature_name,
- messages:
- DiscourseAi::Completions::PromptMessagesBuilder.messages_from_post(
- post,
- style: context_style,
- max_posts: max_context_posts,
- include_uploads: bot.persona.class.vision_enabled,
- bot_usernames: available_bot_usernames,
- ),
- )
-
- reply_user = bot.bot_user
- if bot.persona.class.respond_to?(:user_id)
- reply_user = User.find_by(id: bot.persona.class.user_id) || reply_user
- end
-
- stream_reply = post.topic.private_message? if stream_reply.nil?
-
- # we need to ensure persona user is allowed to reply to the pm
- if post.topic.private_message? && add_user_to_pm
- if !post.topic.topic_allowed_users.exists?(user_id: reply_user.id)
- post.topic.topic_allowed_users.create!(user_id: reply_user.id)
- end
- # edge case, maybe the llm user is missing?
- if !post.topic.topic_allowed_users.exists?(user_id: bot.bot_user.id)
- post.topic.topic_allowed_users.create!(user_id: bot.bot_user.id)
- end
-
- # we store the id of the last bot_user, this is then used to give it preference
- if post.topic.custom_fields[BOT_USER_PREF_ID_CUSTOM_FIELD].to_i != bot.bot_user.id
- post.topic.custom_fields[BOT_USER_PREF_ID_CUSTOM_FIELD] = bot.bot_user.id
- post.topic.save_custom_fields
- end
- end
-
- if stream_reply
- reply_post =
- PostCreator.create!(
- reply_user,
- topic_id: post.topic_id,
- raw: "",
- skip_validations: true,
- skip_jobs: true,
- post_type: post_type,
- skip_guardian: true,
- custom_fields: {
- DiscourseAi::AiBot::POST_AI_LLM_NAME_FIELD => bot.llm.llm_model.display_name,
- },
- )
-
- publish_update(reply_post, { raw: reply_post.cooked })
-
- redis_stream_key = "gpt_cancel:#{reply_post.id}"
- Discourse.redis.setex(redis_stream_key, MAX_STREAM_DELAY_SECONDS, 1)
-
- cancel_manager ||= DiscourseAi::Completions::CancelManager.new
- context.cancel_manager = cancel_manager
- context
- .cancel_manager
- .start_monitor(delay: 0.2) do
- context.cancel_manager.cancel! if !Discourse.redis.get(redis_stream_key)
- end
-
- context.cancel_manager.add_callback(
- lambda { reply_post.update!(raw: reply, cooked: PrettyText.cook(reply)) },
- )
- end
-
- context.skip_tool_details ||= !bot.persona.class.tool_details
- post_streamer = PostStreamer.new(delay: Rails.env.test? ? 0 : 0.5) if stream_reply
- started_thinking = false
-
- new_custom_prompts =
- bot.reply(context) do |partial, placeholder, type|
- if type == :thinking && !started_thinking
- reply << "#{I18n.t("discourse_ai.ai_bot.thinking")}"
- started_thinking = true
- end
-
- if type != :thinking && started_thinking
- reply << "\n\n"
- started_thinking = false
- end
-
- reply << partial
- raw = reply.dup
- raw << "\n\n" << placeholder if placeholder.present?
-
- if blk && type != :tool_details && type != :partial_tool && type != :partial_invoke
- blk.call(partial)
- end
-
- if post_streamer
- post_streamer.run_later do
- Discourse.redis.expire(redis_stream_key, MAX_STREAM_DELAY_SECONDS)
- publish_update(reply_post, { raw: raw })
- end
- end
- end
-
- return if reply.blank? || silent_mode
-
- if stream_reply
- post_streamer.finish
- post_streamer = nil
-
- # land the final message prior to saving so we don't clash
- reply_post.cooked = PrettyText.cook(reply)
- publish_final_update(reply_post)
-
- reply_post.revise(
- bot.bot_user,
- { raw: reply },
- skip_validations: true,
- skip_revision: true,
- )
- else
- reply_post =
- PostCreator.create!(
- reply_user,
- topic_id: post.topic_id,
- raw: reply,
- skip_validations: true,
- post_type: post_type,
- skip_guardian: true,
- )
- end
-
- # a bit messy internally, but this is how we tell
- is_thinking = new_custom_prompts.any? { |prompt| prompt[4].present? }
-
- if is_thinking || new_custom_prompts.length > 1
- reply_post.post_custom_prompt ||= reply_post.build_post_custom_prompt(custom_prompt: [])
- prompt = reply_post.post_custom_prompt.custom_prompt || []
- prompt.concat(new_custom_prompts)
- reply_post.post_custom_prompt.update!(custom_prompt: prompt)
- end
-
- reply_post
- rescue => e
- if reply_post
- details = e.message.to_s
- reply = "#{reply}\n\n#{I18n.t("discourse_ai.ai_bot.reply_error", details: details)}"
- reply_post.revise(
- bot.bot_user,
- { raw: reply },
- skip_validations: true,
- skip_revision: true,
- )
- end
- raise e
- ensure
- context.cancel_manager.stop_monitor if context&.cancel_manager
-
- # since we are skipping validations and jobs we
- # may need to fix participant count
- if reply_post && reply_post.topic && reply_post.topic.private_message? &&
- reply_post.topic.participant_count < 2
- reply_post.topic.update!(participant_count: 2)
- end
- post_streamer&.finish(skip_callback: true)
- publish_final_update(reply_post) if stream_reply
- if reply_post && post.post_number == 1 && post.topic.private_message? && auto_set_title
- title_playground(reply_post, post.user)
- end
- end
-
- def available_bot_usernames
- @bot_usernames ||=
- AiPersona.joins(:user).pluck(:username).concat(available_bot_users.map(&:username))
- end
-
- def available_bot_user_ids
- @bot_ids ||= AiPersona.joins(:user).pluck("users.id").concat(available_bot_users.map(&:id))
- end
-
- private
-
- def available_bot_users
- @available_bots ||=
- User.joins("INNER JOIN llm_models llm ON llm.user_id = users.id").where(active: true)
- end
-
- def publish_final_update(reply_post)
- return if @published_final_update
- if reply_post
- publish_update(reply_post, { cooked: reply_post.cooked, done: true })
- # we subscribe at position -2 so we will always get this message
- # moving all cooked on every page load is wasteful ... this means
- # we have a benign message at the end, 2 is set to ensure last message
- # is delivered
- publish_update(reply_post, { noop: true })
- @published_final_update = true
- end
- end
-
- def can_attach?(post)
- return false if bot.bot_user.nil?
- return false if post.topic.private_message? && post.post_type != Post.types[:regular]
- return false if (SiteSetting.ai_bot_allowed_groups_map & post.user.group_ids).blank?
- return false if post.custom_fields[BYPASS_AI_REPLY_CUSTOM_FIELD].present?
-
- true
- end
-
- def schedule_bot_reply(post)
- persona_id =
- DiscourseAi::Personas::Persona.system_personas[bot.persona.class] || bot.persona.class.id
- ::Jobs.enqueue(
- :create_ai_reply,
- post_id: post.id,
- bot_user_id: bot.bot_user.id,
- persona_id: persona_id,
- )
- end
-
- def context(topic)
- {
- site_url: Discourse.base_url,
- site_title: SiteSetting.title,
- site_description: SiteSetting.site_description,
- time: Time.zone.now,
- participants: topic.allowed_users.map(&:username).join(", "),
- }
- end
-
- def publish_update(bot_reply_post, payload)
- payload = { post_id: bot_reply_post.id, post_number: bot_reply_post.post_number }.merge(
- payload,
- )
- MessageBus.publish(
- "discourse-ai/ai-bot/topic/#{bot_reply_post.topic_id}",
- payload,
- user_ids: bot_reply_post.topic.allowed_user_ids,
- max_backlog_size: 2,
- max_backlog_age: MAX_STREAM_DELAY_SECONDS,
- )
- end
- end
- end
-end
diff --git a/lib/ai_bot/post_streamer.rb b/lib/ai_bot/post_streamer.rb
deleted file mode 100644
index 73621a2f..00000000
--- a/lib/ai_bot/post_streamer.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiBot
- class PostStreamer
- # test only
- def self.on_callback=(on_callback)
- @on_callback = on_callback
- end
-
- def self.on_callback
- @on_callback
- end
-
- def initialize(delay: 0.5)
- @mutex = Mutex.new
- @callback = nil
- @delay = delay
- @done = false
- end
-
- def run_later(&callback)
- self.class.on_callback.call(callback) if self.class.on_callback
- @mutex.synchronize { @callback = callback }
- ensure_worker!
- end
-
- def finish(skip_callback: false)
- @mutex.synchronize do
- @callback&.call if skip_callback
- @callback = nil
- @done = true
- end
-
- begin
- @worker_thread&.wakeup
- rescue StandardError
- ThreadError
- end
- @worker_thread&.join
- @worker_thread = nil
- end
-
- private
-
- def run
- while !@done
- @mutex.synchronize do
- callback = @callback
- @callback = nil
- callback&.call
- end
- sleep @delay
- end
- end
-
- def ensure_worker!
- return if @worker_thread
- @mutex.synchronize do
- return if @worker_thread
- db = RailsMultisite::ConnectionManagement.current_db
- @worker_thread =
- Thread.new { RailsMultisite::ConnectionManagement.with_connection(db) { run } }
- end
- end
- end
- end
-end
diff --git a/lib/ai_bot/response_http_streamer.rb b/lib/ai_bot/response_http_streamer.rb
deleted file mode 100644
index cfb2fbeb..00000000
--- a/lib/ai_bot/response_http_streamer.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiBot
- class ResponseHttpStreamer
- CRLF = "\r\n"
- POOL_SIZE = 10
-
- class << self
- def thread_pool
- # we use our thread pool implementation here for a few reasons:
- #
- # 1. Free multisite support
- # 2. Unlike Concurrent::CachedThreadPool, we spin back down to 0 threads automatiaclly see: https://github.com/ruby-concurrency/concurrent-ruby/issues/1075
- # 3. Better internal error handling
- @thread_pool ||=
- Scheduler::ThreadPool.new(min_threads: 0, max_threads: POOL_SIZE, idle_time: 30)
- end
-
- def schedule_block(&block)
- thread_pool.post do
- begin
- block.call
- rescue StandardError => e
- Discourse.warn_exception(e, message: "Discourse AI: Unable to stream reply")
- end
- end
- end
-
- # keeping this in a static method so we don't capture ENV and other bits
- # this allows us to release memory earlier
- def queue_streamed_reply(
- io:,
- persona:,
- user:,
- topic:,
- query:,
- custom_instructions:,
- current_user:
- )
- schedule_block do
- begin
- post_params = {
- raw: query,
- skip_validations: true,
- custom_fields: {
- DiscourseAi::AiBot::Playground::BYPASS_AI_REPLY_CUSTOM_FIELD => true,
- },
- }
-
- if topic
- post_params[:topic_id] = topic.id
- else
- post_params[:title] = I18n.t("discourse_ai.ai_bot.default_pm_prefix")
- post_params[:archetype] = Archetype.private_message
- post_params[:target_usernames] = "#{user.username},#{persona.user.username}"
- end
-
- post = PostCreator.create!(user, post_params)
- topic = post.topic
-
- io.write "HTTP/1.1 200 OK"
- io.write CRLF
- io.write "Content-Type: text/plain; charset=utf-8"
- io.write CRLF
- io.write "Transfer-Encoding: chunked"
- io.write CRLF
- io.write "Cache-Control: no-cache, no-store, must-revalidate"
- io.write CRLF
- io.write "Connection: close"
- io.write CRLF
- io.write "X-Accel-Buffering: no"
- io.write CRLF
- io.write "X-Content-Type-Options: nosniff"
- io.write CRLF
- io.write CRLF
- io.flush
-
- persona_class =
- DiscourseAi::Personas::Persona.find_by(id: persona.id, user: current_user)
- bot = DiscourseAi::Personas::Bot.as(persona.user, persona: persona_class.new)
-
- data =
- {
- topic_id: topic.id,
- bot_user_id: persona.user.id,
- persona_id: persona.id,
- }.to_json + "\n\n"
-
- io.write data.bytesize.to_s(16)
- io.write CRLF
- io.write data
- io.write CRLF
-
- DiscourseAi::AiBot::Playground
- .new(bot)
- .reply_to(post, custom_instructions: custom_instructions) do |partial|
- next if partial.length == 0
-
- data = { partial: partial }.to_json + "\n\n"
-
- data.force_encoding("UTF-8")
-
- io.write data.bytesize.to_s(16)
- io.write CRLF
- io.write data
- io.write CRLF
- io.flush
- end
-
- io.write "0"
- io.write CRLF
- io.write CRLF
-
- io.flush
- io.done if io.respond_to?(:done)
- rescue StandardError => e
- # make it a tiny bit easier to debug in dev, this is tricky
- # multi-threaded code that exhibits various limitations in rails
- p e if Rails.env.development? || Rails.env.test?
- Discourse.warn_exception(e, message: "Discourse AI: Unable to stream reply")
- ensure
- io.close
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/ai_bot/site_settings_extension.rb b/lib/ai_bot/site_settings_extension.rb
deleted file mode 100644
index 4aa3653c..00000000
--- a/lib/ai_bot/site_settings_extension.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi::AiBot::SiteSettingsExtension
- def self.enable_or_disable_ai_bots
- LlmModel.find_each { |llm_model| llm_model.toggle_companion_user }
- end
-end
diff --git a/lib/ai_helper/assistant.rb b/lib/ai_helper/assistant.rb
deleted file mode 100644
index be61e415..00000000
--- a/lib/ai_helper/assistant.rb
+++ /dev/null
@@ -1,469 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiHelper
- class Assistant
- IMAGE_CAPTION_MAX_WORDS = 50
-
- TRANSLATE = "translate"
- GENERATE_TITLES = "generate_titles"
- PROOFREAD = "proofread"
- MARKDOWN_TABLE = "markdown_table"
- CUSTOM_PROMPT = "custom_prompt"
- EXPLAIN = "explain"
- ILLUSTRATE_POST = "illustrate_post"
- REPLACE_DATES = "replace_dates"
- IMAGE_CAPTION = "image_caption"
-
- def self.prompt_cache
- @prompt_cache ||= ::DiscourseAi::MultisiteHash.new("prompt_cache")
- end
-
- def self.clear_prompt_cache!
- prompt_cache.flush!
- end
-
- def initialize(helper_llm: nil, image_caption_llm: nil)
- @helper_llm = helper_llm
- @image_caption_llm = image_caption_llm
- end
-
- def available_prompts(user)
- key = "prompt_cache_#{I18n.locale}"
- prompts = self.class.prompt_cache.fetch(key) { self.all_prompts }
-
- prompts
- .map do |prompt|
- next if !user.in_any_groups?(prompt[:allowed_group_ids])
-
- if prompt[:name] == ILLUSTRATE_POST &&
- SiteSetting.ai_helper_illustrate_post_model == "disabled"
- next
- end
-
- # We cannot cache this. It depends on the user's effective_locale.
- if prompt[:name] == TRANSLATE
- locale = user.effective_locale
- locale_hash =
- LocaleSiteSetting.language_names[locale] ||
- LocaleSiteSetting.language_names[locale.split("_")[0]]
- translation =
- I18n.t(
- "discourse_ai.ai_helper.prompts.translate",
- language: locale_hash["nativeName"],
- ) || prompt[:name]
-
- prompt.merge(translated_name: translation)
- else
- prompt
- end
- end
- .compact
- end
-
- def custom_locale_instructions(user = nil, force_default_locale)
- locale = SiteSetting.default_locale
- locale = user.effective_locale if !force_default_locale && user
- locale_hash = LocaleSiteSetting.language_names[locale]
-
- if locale != "en" && locale_hash
- locale_description = "#{locale_hash["name"]} (#{locale_hash["nativeName"]})"
- "It is imperative that you write your answer in #{locale_description}, you are interacting with a #{locale_description} speaking user. Leave tag names in English."
- else
- nil
- end
- end
-
- def attach_user_context(context, user = nil, force_default_locale: false)
- locale = SiteSetting.default_locale
- locale = user.effective_locale if user && !force_default_locale
- locale_hash = LocaleSiteSetting.language_names[locale]
-
- context.user_language = "#{locale_hash["name"]}"
-
- if user
- timezone = user&.user_option&.timezone || "UTC"
- current_time = Time.now.in_time_zone(timezone)
-
- temporal_context = {
- utc_date_time: current_time.iso8601,
- local_time: current_time.strftime("%H:%M"),
- user: {
- timezone: timezone,
- weekday: current_time.strftime("%A"),
- },
- }
-
- context.temporal_context = temporal_context.to_json
- end
-
- context
- end
-
- def generate_prompt(
- helper_mode,
- input,
- user,
- force_default_locale: false,
- custom_prompt: nil,
- &block
- )
- bot = build_bot(helper_mode, user)
-
- user_input = "#{input}"
- if helper_mode == CUSTOM_PROMPT && custom_prompt.present?
- user_input = "#{custom_prompt}:\n#{input}"
- end
-
- context =
- DiscourseAi::Personas::BotContext.new(
- user: user,
- skip_tool_details: true,
- feature_name: "ai_helper",
- messages: [{ type: :user, content: user_input }],
- format_dates: helper_mode == REPLACE_DATES,
- custom_instructions: custom_locale_instructions(user, force_default_locale),
- )
- context = attach_user_context(context, user, force_default_locale: force_default_locale)
-
- bad_json = false
- json_summary_schema_key = bot.persona.response_format&.first.to_h
-
- schema_key = json_summary_schema_key["key"]&.to_sym
- schema_type = json_summary_schema_key["type"]
-
- if schema_type == "array"
- helper_response = []
- else
- helper_response = +""
- end
-
- buffer_blk =
- Proc.new do |partial, _, type|
- if type == :structured_output && schema_type
- helper_chunk = partial.read_buffered_property(schema_key)
- next if helper_chunk.nil? || helper_chunk.empty?
-
- if schema_type == "array"
- if helper_chunk.is_a?(Array)
- helper_chunk.each do |item|
- helper_response << item if helper_response.exclude?(item)
- end
- end
- elsif schema_type == "string"
- helper_response << helper_chunk
- else
- helper_response = helper_chunk
- end
-
- block.call(helper_chunk) if block && !bad_json
- elsif type.blank?
- # Assume response is a regular completion.
- helper_response << partial
- block.call(partial) if block
- end
- end
-
- bot.reply(context, &buffer_blk)
-
- helper_response
- end
-
- def generate_and_send_prompt(
- helper_mode,
- input,
- user,
- force_default_locale: false,
- custom_prompt: nil
- )
- helper_response =
- generate_prompt(
- helper_mode,
- input,
- user,
- force_default_locale: force_default_locale,
- custom_prompt: custom_prompt,
- )
- result = { type: prompt_type(helper_mode) }
-
- result[:suggestions] = (
- if result[:type] == :list
- helper_response.flatten.map { |suggestion| sanitize_result(suggestion) }
- else
- sanitized = sanitize_result(helper_response)
- result[:diff] = parse_diff(input, sanitized) if result[:type] == :diff
- [sanitized]
- end
- )
-
- result
- end
-
- def stream_prompt(
- helper_mode,
- input,
- user,
- channel,
- force_default_locale: false,
- client_id: nil,
- custom_prompt: nil
- )
- streamed_diff = +""
- streamed_result = +""
- start = Time.now
- type = prompt_type(helper_mode)
-
- generate_prompt(
- helper_mode,
- input,
- user,
- force_default_locale: force_default_locale,
- custom_prompt: custom_prompt,
- ) do |partial_response|
- streamed_result << partial_response
- streamed_diff = parse_diff(input, partial_response) if type == :diff
-
- # Throttle updates and check for safe stream points
- if (streamed_result.length > 10 && (Time.now - start > 0.3)) || Rails.env.test?
- sanitized = sanitize_result(streamed_result)
-
- payload = { result: sanitized, diff: streamed_diff, done: false }
- publish_update(channel, payload, user, client_id: client_id)
- start = Time.now
- end
- end
-
- final_diff = parse_diff(input, streamed_result) if type == :diff
-
- sanitized_result = sanitize_result(streamed_result)
- if sanitized_result.present?
- publish_update(
- channel,
- { result: sanitized_result, diff: final_diff, done: true },
- user,
- client_id: client_id,
- )
- end
- end
-
- def generate_image_caption(upload, user)
- bot = build_bot(IMAGE_CAPTION, user)
- force_default_locale = false
-
- context =
- DiscourseAi::Personas::BotContext.new(
- user: user,
- skip_tool_details: true,
- feature_name: IMAGE_CAPTION,
- messages: [
- {
- type: :user,
- content: ["Describe this image in a single sentence.", { upload_id: upload.id }],
- },
- ],
- custom_instructions: custom_locale_instructions(user, force_default_locale),
- )
-
- structured_output = nil
-
- buffer_blk =
- Proc.new do |partial, _, type|
- if type == :structured_output
- structured_output = partial
- bot.persona.response_format&.first.to_h
- end
- end
-
- bot.reply(context, llm_args: { max_tokens: 1024 }, &buffer_blk)
-
- raw_caption = ""
-
- if structured_output
- json_summary_schema_key = bot.persona.response_format&.first.to_h
- raw_caption =
- structured_output.read_buffered_property(json_summary_schema_key["key"]&.to_sym)
- end
-
- raw_caption.delete("|").squish.truncate_words(IMAGE_CAPTION_MAX_WORDS)
- end
-
- private
-
- def build_bot(helper_mode, user)
- persona_id = personas_prompt_map(include_image_caption: true).invert[helper_mode]
- raise Discourse::InvalidParameters.new(:mode) if persona_id.blank?
-
- persona_klass = AiPersona.find_by(id: persona_id)&.class_instance
- return if persona_klass.nil?
-
- llm_model = find_ai_helper_model(helper_mode, persona_klass)
-
- DiscourseAi::Personas::Bot.as(user, persona: persona_klass.new, model: llm_model)
- end
-
- def find_ai_helper_model(helper_mode, persona_klass)
- if helper_mode == IMAGE_CAPTION && @image_caption_llm.is_a?(LlmModel)
- return @image_caption_llm
- end
-
- return @helper_llm if helper_mode != IMAGE_CAPTION && @helper_llm.is_a?(LlmModel)
- self.class.find_ai_helper_model(helper_mode, persona_klass)
- end
-
- # Priorities are:
- # 1. Persona's default LLM
- # 2. Hidden `ai_helper_model` setting, or `ai_helper_image_caption_model` for image_caption.
- # 3. Newest LLM config
- def self.find_ai_helper_model(helper_mode, persona_klass)
- model_id = persona_klass.default_llm_id
-
- if !model_id
- if helper_mode == IMAGE_CAPTION
- model_id = SiteSetting.ai_helper_image_caption_model&.split(":")&.last
- else
- model_id = SiteSetting.ai_helper_model&.split(":")&.last
- end
- end
-
- if model_id.present?
- LlmModel.find_by(id: model_id)
- else
- LlmModel.last
- end
- end
-
- def personas_prompt_map(include_image_caption: false)
- map = {
- SiteSetting.ai_helper_translator_persona.to_i => TRANSLATE,
- SiteSetting.ai_helper_title_suggestions_persona.to_i => GENERATE_TITLES,
- SiteSetting.ai_helper_proofreader_persona.to_i => PROOFREAD,
- SiteSetting.ai_helper_markdown_tables_persona.to_i => MARKDOWN_TABLE,
- SiteSetting.ai_helper_custom_prompt_persona.to_i => CUSTOM_PROMPT,
- SiteSetting.ai_helper_explain_persona.to_i => EXPLAIN,
- SiteSetting.ai_helper_post_illustrator_persona.to_i => ILLUSTRATE_POST,
- SiteSetting.ai_helper_smart_dates_persona.to_i => REPLACE_DATES,
- }
-
- if include_image_caption
- image_caption_persona = SiteSetting.ai_helper_image_caption_persona.to_i
- map[image_caption_persona] = IMAGE_CAPTION if image_caption_persona
- end
-
- map
- end
-
- def all_prompts
- AiPersona
- .where(id: personas_prompt_map.keys)
- .map do |ai_persona|
- prompt_name = personas_prompt_map[ai_persona.id]
-
- if prompt_name
- {
- name: prompt_name,
- translated_name:
- I18n.t("discourse_ai.ai_helper.prompts.#{prompt_name}", default: nil) ||
- prompt_name,
- prompt_type: prompt_type(prompt_name),
- icon: icon_map(prompt_name),
- location: location_map(prompt_name),
- allowed_group_ids: ai_persona.allowed_group_ids,
- }
- end
- end
- .compact
- end
-
- SANITIZE_REGEX_STR =
- %w[term context topic replyTo input output result]
- .map { |tag| "<#{tag}>\\n?|\\n?#{tag}>" }
- .join("|")
-
- SANITIZE_REGEX = Regexp.new(SANITIZE_REGEX_STR, Regexp::IGNORECASE | Regexp::MULTILINE)
-
- def sanitize_result(result)
- result.gsub(SANITIZE_REGEX, "")
- end
-
- def publish_update(channel, payload, user, client_id: nil)
- # when publishing we make sure we do not keep large backlogs on the channel
- # and make sure we clear the streaming info after 60 seconds
- # this ensures we do not bloat redis
- if client_id
- MessageBus.publish(
- channel,
- payload,
- user_ids: [user.id],
- client_ids: [client_id],
- max_backlog_age: 60,
- )
- else
- MessageBus.publish(channel, payload, user_ids: [user.id], max_backlog_age: 60)
- end
- end
-
- def icon_map(name)
- case name
- when TRANSLATE
- "language"
- when GENERATE_TITLES
- "heading"
- when PROOFREAD
- "spell-check"
- when MARKDOWN_TABLE
- "table"
- when CUSTOM_PROMPT
- "comment"
- when EXPLAIN
- "question"
- when ILLUSTRATE_POST
- "images"
- when REPLACE_DATES
- "calendar-days"
- else
- nil
- end
- end
-
- def location_map(name)
- case name
- when TRANSLATE
- %w[composer post]
- when GENERATE_TITLES
- %w[composer]
- when PROOFREAD
- %w[composer post]
- when MARKDOWN_TABLE
- %w[composer]
- when CUSTOM_PROMPT
- %w[composer post]
- when EXPLAIN
- %w[post]
- when ILLUSTRATE_POST
- %w[composer]
- when REPLACE_DATES
- %w[composer]
- else
- %w[]
- end
- end
-
- def prompt_type(prompt_name)
- if [PROOFREAD, MARKDOWN_TABLE, REPLACE_DATES, CUSTOM_PROMPT].include?(prompt_name)
- return :diff
- end
-
- return :list if [ILLUSTRATE_POST, GENERATE_TITLES].include?(prompt_name)
-
- :text
- end
-
- def parse_diff(text, suggestion)
- cooked_text = PrettyText.cook(text)
- cooked_suggestion = PrettyText.cook(suggestion)
-
- DiscourseDiff.new(cooked_text, cooked_suggestion).inline_html
- end
- end
- end
-end
diff --git a/lib/ai_helper/chat_thread_titler.rb b/lib/ai_helper/chat_thread_titler.rb
deleted file mode 100644
index 15ffc52c..00000000
--- a/lib/ai_helper/chat_thread_titler.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiHelper
- class ChatThreadTitler
- def initialize(thread)
- @thread = thread
- end
-
- def suggested_title
- content = thread_content(@thread)
- return nil if content.blank?
-
- suggested_title = call_llm(content)
- cleanup(suggested_title)
- end
-
- def call_llm(thread_content)
- chat = "\n#{thread_content}\n"
-
- prompt =
- DiscourseAi::Completions::Prompt.new(
- <<~TEXT.strip,
- I want you to act as a title generator for chat between users. I will provide you with the chat transcription,
- and you will generate a single attention-grabbing title. Please keep the title concise and under 15 words
- and ensure that the meaning is maintained. The title will utilize the same language type of the chat.
- I want you to only reply the suggested title and nothing else, do not write explanations.
- You will find the chat between XML tags. Return the suggested title between tags.
- TEXT
- messages: [{ type: :user, content: chat, id: "User" }],
- )
-
- DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_helper_model).generate(
- prompt,
- user: Discourse.system_user,
- stop_sequences: [""],
- feature_name: "chat_thread_title",
- )
- end
-
- def cleanup(title)
- (Nokogiri::HTML5.fragment(title).at("title")&.text || title)
- .split("\n")
- .first
- .then { _1.match?(/^("|')(.*)("|')$/) ? _1[1..-2] : _1 }
- .truncate(100, separator: " ")
- end
-
- def thread_content(thread)
- # TODO: Replace me by a proper API call
- thread
- .chat_messages
- .joins(:user)
- .pluck(:username, :message)
- .map { |username, message| "#{username}: #{message}" }
- .join("\n")
- end
-
- attr_reader :thread
- end
- end
-end
diff --git a/lib/ai_helper/date_formatter.rb b/lib/ai_helper/date_formatter.rb
deleted file mode 100644
index 382fb4d4..00000000
--- a/lib/ai_helper/date_formatter.rb
+++ /dev/null
@@ -1,144 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiHelper
- class DateFormatter
- DAYS_OF_WEEK = {
- "monday" => 1,
- "tuesday" => 2,
- "wednesday" => 3,
- "thursday" => 4,
- "friday" => 5,
- "saturday" => 6,
- "sunday" => 0,
- }
-
- class << self
- def process_date_placeholders(text, user)
- return text if !text.include?("{{")
-
- timezone = user.user_option.timezone || "UTC"
- reference_time = Time.now.in_time_zone(timezone)
-
- text.gsub(
- /\{\{(date_time_offset_minutes|date_offset_days|datetime|date|next_week):([^}]+)\}\}/,
- ) do |match|
- type = $1
- value = $2
-
- case type
- when "datetime"
- if value.include?(":")
- # Handle range like "2pm+1:3pm+2"
- start_str, end_str = value.split(":")
- format_datetime_range(
- parse_time_with_offset(start_str, reference_time),
- parse_time_with_offset(end_str, reference_time),
- timezone,
- )
- else
- # Handle single time like "2pm+1" or "10pm"
- format_date_time(parse_time_with_offset(value, reference_time), timezone)
- end
- when "next_week"
- if value.include?(":")
- # Handle range like "tuesday-1pm:tuesday-3pm"
- start_str, end_str = value.split(":")
- start_time = parse_next_week(start_str, reference_time)
- end_time = parse_next_week(end_str, reference_time)
- format_datetime_range(start_time, end_time, timezone)
- else
- # Handle single time like "tuesday-1pm" or just "tuesday"
- time = parse_next_week(value, reference_time)
- value.include?("-") ? format_date_time(time, timezone) : format_date(time, timezone)
- end
- when "date"
- format_date(reference_time + value.to_i.days, timezone)
- when "date_time_offset_minutes"
- if value.include?(":")
- start_offset, end_offset = value.split(":").map(&:to_i)
- format_datetime_range(
- reference_time + start_offset.minutes,
- reference_time + end_offset.minutes,
- timezone,
- )
- else
- format_date_time(reference_time + value.to_i.minutes, timezone)
- end
- when "date_offset_days"
- if value.include?(":")
- start_offset, end_offset = value.split(":").map(&:to_i)
- format_date_range(
- reference_time + start_offset.days,
- reference_time + end_offset.days,
- timezone,
- )
- else
- format_date(reference_time + value.to_i.days, timezone)
- end
- end
- end
- end
-
- private
-
- def parse_next_week(str, reference_time)
- if str.include?("-")
- # Handle day with time like "tuesday-1pm"
- day, time = str.split("-")
- target_date = get_next_week_day(day.downcase, reference_time)
- parse_time(time, target_date)
- else
- # Just the day
- get_next_week_day(str.downcase, reference_time)
- end
- end
-
- def get_next_week_day(day, reference_time)
- raise ArgumentError unless DAYS_OF_WEEK.key?(day)
-
- target_date = reference_time + 1.week
- days_ahead = DAYS_OF_WEEK[day] - target_date.wday
- days_ahead += 7 if days_ahead < 0
- target_date + days_ahead.days
- end
-
- def parse_time_with_offset(time_str, reference_time)
- if time_str.include?("+")
- time_part, days = time_str.split("+")
- parse_time(time_part, reference_time + days.to_i.days)
- else
- parse_time(time_str, reference_time)
- end
- end
-
- def parse_time(time_str, reference_time)
- hour = time_str.to_i
- if time_str.downcase.include?("pm") && hour != 12
- hour += 12
- elsif time_str.downcase.include?("am") && hour == 12
- hour = 0
- end
-
- reference_time.change(hour: hour, min: 0, sec: 0)
- end
-
- def format_date(time, timezone)
- "[date=#{time.strftime("%Y-%m-%d")} timezone=\"#{timezone}\"]"
- end
-
- def format_date_time(time, timezone)
- "[date=#{time.strftime("%Y-%m-%d")} time=#{time.strftime("%H:%M:%S")} timezone=\"#{timezone}\"]"
- end
-
- def format_date_range(start_time, end_time, timezone)
- "[date-range from=#{start_time.strftime("%Y-%m-%d")} to=#{end_time.strftime("%Y-%m-%d")} timezone=\"#{timezone}\"]"
- end
-
- def format_datetime_range(start_time, end_time, timezone)
- "[date-range from=#{start_time.strftime("%Y-%m-%dT%H:%M:%S")} to=#{end_time.strftime("%Y-%m-%dT%H:%M:%S")} timezone=\"#{timezone}\"]"
- end
- end
- end
- end
-end
diff --git a/lib/ai_helper/entry_point.rb b/lib/ai_helper/entry_point.rb
deleted file mode 100644
index cef7e2a4..00000000
--- a/lib/ai_helper/entry_point.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi
- module AiHelper
- class EntryPoint
- def inject_into(plugin)
- plugin.register_seedfu_fixtures(
- Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "ai_helper"),
- )
-
- plugin.add_to_serializer(:current_user, :can_use_assistant) do
- scope.user.in_any_groups?(SiteSetting.composer_ai_helper_allowed_groups_map)
- end
-
- plugin.add_to_serializer(:current_user, :can_use_assistant_in_post) do
- scope.user.in_any_groups?(SiteSetting.post_ai_helper_allowed_groups_map)
- end
-
- plugin.add_to_serializer(:current_user, :can_use_custom_prompts) do
- scope.user.in_any_groups?(SiteSetting.ai_helper_custom_prompts_allowed_groups_map)
- end
-
- plugin.on(:chat_message_created) do |message, channel, user, extra|
- next unless SiteSetting.ai_helper_enabled
- next unless SiteSetting.ai_helper_automatic_chat_thread_title
- next if extra[:thread].blank?
- next if extra[:thread].title.present?
-
- reply_count = extra[:thread].replies.count
-
- if reply_count.between?(1, 4)
- ::Jobs.enqueue_in(
- SiteSetting.ai_helper_automatic_chat_thread_title_delay.minutes,
- :generate_chat_thread_title,
- thread_id: extra[:thread].id,
- )
- elsif reply_count >= 5
- ::Jobs.enqueue(:generate_chat_thread_title, thread_id: extra[:thread].id)
- end
- end
-
- plugin.add_to_serializer(
- :current_user,
- :ai_helper_prompts,
- include_condition: -> { SiteSetting.ai_helper_enabled && scope.authenticated? },
- ) do
- ActiveModel::ArraySerializer.new(
- DiscourseAi::AiHelper::Assistant.new.available_prompts(scope.user),
- root: false,
- )
- end
-
- plugin.add_to_serializer(:current_user, :user_allowed_ai_auto_image_captions) do
- scope.user.in_any_groups?(SiteSetting.ai_auto_image_caption_allowed_groups_map)
- end
-
- UserUpdater::OPTION_ATTR.push(:auto_image_caption)
- plugin.add_to_serializer(
- :user_option,
- :auto_image_caption,
- include_condition: -> do
- SiteSetting.ai_helper_enabled &&
- SiteSetting.ai_helper_enabled_features.include?("image_caption") &&
- scope.user.in_any_groups?(SiteSetting.ai_auto_image_caption_allowed_groups_map)
- end,
- ) { object.auto_image_caption }
-
- plugin.add_to_serializer(
- :current_user_option,
- :auto_image_caption,
- include_condition: -> do
- SiteSetting.ai_helper_enabled &&
- SiteSetting.ai_helper_enabled_features.include?("image_caption") &&
- scope.user.in_any_groups?(SiteSetting.ai_auto_image_caption_allowed_groups_map)
- end,
- ) { object.auto_image_caption }
- end
- end
- end
-end
diff --git a/lib/ai_helper/painter.rb b/lib/ai_helper/painter.rb
deleted file mode 100644
index 9be8b95a..00000000
--- a/lib/ai_helper/painter.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiHelper
- class Painter
- def commission_thumbnails(input, user)
- return [] if input.blank?
-
- model = SiteSetting.ai_helper_illustrate_post_model
-
- if model == "stable_diffusion_xl"
- stable_diffusion_prompt = diffusion_prompt(input, user)
- return [] if stable_diffusion_prompt.blank?
-
- artifacts =
- DiscourseAi::Inference::StabilityGenerator
- .perform!(stable_diffusion_prompt)
- .dig(:artifacts)
- .to_a
- .map { |art| art[:base64] }
-
- base64_to_image(artifacts, user.id)
- elsif model == "dall_e_3"
- attribution =
- I18n.t(
- "discourse_ai.ai_helper.painter.attribution.#{SiteSetting.ai_helper_illustrate_post_model}",
- )
- results =
- DiscourseAi::Inference::OpenAiImageGenerator.create_uploads!(
- input,
- model: "dall-e-3",
- user_id: user.id,
- title: attribution,
- )
- results.map { |result| UploadSerializer.new(result[:upload], root: false) }
- end
- end
-
- private
-
- def base64_to_image(artifacts, user_id)
- attribution =
- I18n.t(
- "discourse_ai.ai_helper.painter.attribution.#{SiteSetting.ai_helper_illustrate_post_model}",
- )
-
- artifacts.each_with_index.map do |art, i|
- f = Tempfile.new("v1_txt2img_#{i}.png")
- f.binmode
- f.write(Base64.decode64(art))
- f.rewind
- upload = UploadCreator.new(f, attribution).create_for(user_id)
- f.unlink
-
- UploadSerializer.new(upload, root: false)
- end
- end
-
- def diffusion_prompt(text, user)
- prompt =
- DiscourseAi::Completions::Prompt.new(
- <<~TEXT.strip,
- Provide me a StableDiffusion prompt to generate an image that illustrates the following post in 40 words or less, be creative.
- You'll find the post between XML tags.
- TEXT
- messages: [{ type: :user, content: text, id: user.username }],
- )
-
- DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_helper_model).generate(
- prompt,
- user: user,
- feature_name: "illustrate_post",
- )
- end
- end
- end
-end
diff --git a/lib/ai_helper/semantic_categorizer.rb b/lib/ai_helper/semantic_categorizer.rb
deleted file mode 100644
index 488741de..00000000
--- a/lib/ai_helper/semantic_categorizer.rb
+++ /dev/null
@@ -1,145 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi
- module AiHelper
- class SemanticCategorizer
- def initialize(user, opts)
- @user = user
- @text = opts[:text]
- @vector = DiscourseAi::Embeddings::Vector.instance
- @schema = DiscourseAi::Embeddings::Schema.for(Topic)
- @topic_id = opts[:topic_id]
- end
-
- def categories
- return [] if @text.blank? && @topic_id.nil?
- return [] if !DiscourseAi::Embeddings.enabled?
-
- candidates = nearest_neighbors
- return [] if candidates.empty?
-
- candidate_ids = candidates.map(&:first)
-
- ::Topic
- .joins(:category)
- .where(id: candidate_ids)
- .where("categories.id IN (?)", Category.topic_create_allowed(@user.guardian).pluck(:id))
- .order("array_position(ARRAY#{candidate_ids}, topics.id)")
- .pluck(
- "categories.id",
- "categories.name",
- "categories.slug",
- "categories.color",
- "categories.topic_count",
- )
- .map
- .with_index do |(id, name, slug, color, topic_count), index|
- {
- id: id,
- name: name,
- slug: slug,
- color: color,
- topicCount: topic_count,
- score: candidates[index].last,
- }
- end
- .map do |c|
- # Note: <#> returns the negative inner product since Postgres only supports ASC order index scans on operators
- c[:score] = (c[:score] + 1).abs if @vector.vdef.pg_function = "<#>"
-
- c[:score] = 1 / (c[:score] + 1) # inverse of the distance
- c
- end
- .group_by { |c| c[:name] }
- .map { |name, scores| scores.first.merge(score: scores.sum { |s| s[:score] }) }
- .sort_by { |c| -c[:score] }
- .take(5)
- end
-
- def tags
- return [] if @text.blank? && @topic_id.nil?
- return [] if !DiscourseAi::Embeddings.enabled?
-
- candidates = nearest_neighbors(limit: 100)
- return [] if candidates.empty?
-
- candidate_ids = candidates.map(&:first)
-
- count_column = Tag.topic_count_column(@user.guardian) # Determine the count column
-
- ::Topic
- .joins(:topic_tags, :tags)
- .where(id: candidate_ids)
- .where("tags.id IN (?)", DiscourseTagging.visible_tags(@user.guardian).pluck(:id))
- .group("topics.id")
- .order("array_position(ARRAY#{candidate_ids}, topics.id)")
- .pluck("array_agg(tags.name)")
- .map(&:uniq)
- .map
- .with_index { |tag_list, index| { tags: tag_list, score: candidates[index].last } }
- .flat_map { |c| c[:tags].map { |t| { name: t, score: c[:score] } } }
- .map do |c|
- # Note: <#> returns the negative inner product since Postgres only supports ASC order index scans on operators
- c[:score] = (c[:score] + 1).abs if @vector.vdef.pg_function = "<#>"
-
- c[:score] = 1 / (c[:score] + 1) # inverse of the distance
- c
- end
- .group_by { |c| c[:name] }
- .map { |name, scores| { name: name, score: scores.sum { |s| s[:score] } } }
- .sort_by { |c| -c[:score] }
- .take(7)
- .then do |tags|
- models = Tag.where(name: tags.map { _1[:name] }).index_by(&:name)
- tags.map do |tag|
- tag[:id] = models.dig(tag[:name])&.id
- tag[:count] = models.dig(tag[:name])&.public_send(count_column) || 0
- tag
- end
- end
- end
-
- private
-
- def nearest_neighbors(limit: 50)
- if @topic_id
- target = Topic.find_by(id: @topic_id)
- embeddings = @schema.find_by_target(target)&.embeddings
-
- if embeddings.blank?
- @text =
- DiscourseAi::Summarization::Strategies::TopicSummary
- .new(target)
- .targets_data
- .pluck(:text)
- raw_vector = @vector.vector_from(@text)
- else
- raw_vector = JSON.parse(embeddings)
- end
- else
- raw_vector = @vector.vector_from(@text)
- end
-
- muted_category_ids = nil
- if @user.present?
- muted_category_ids =
- CategoryUser.where(
- user: @user,
- notification_level: CategoryUser.notification_levels[:muted],
- ).pluck(:category_id)
- end
-
- @schema
- .asymmetric_similarity_search(raw_vector, limit: limit, offset: 0) do |builder|
- builder.join("topics t on t.id = topic_id")
- unless muted_category_ids.empty?
- builder.where(<<~SQL, exclude_category_ids: muted_category_ids.map(&:to_i))
- t.category_id NOT IN (:exclude_category_ids) AND
- t.category_id NOT IN (SELECT categories.id FROM categories WHERE categories.parent_category_id IN (:exclude_category_ids))
- SQL
- end
- end
- .map { |r| [r.topic_id, r.distance] }
- end
- end
- end
-end
diff --git a/lib/ai_moderation/entry_point.rb b/lib/ai_moderation/entry_point.rb
deleted file mode 100644
index 122d6f3f..00000000
--- a/lib/ai_moderation/entry_point.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiModeration
- class EntryPoint
- def inject_into(plugin)
- plugin.on(:post_created) { |post| ::DiscourseAi::AiModeration::SpamScanner.new_post(post) }
- plugin.on(:post_edited) do |post|
- ::DiscourseAi::AiModeration::SpamScanner.edited_post(post)
- end
- plugin.on(:post_process_cooked) do |_doc, post|
- ::DiscourseAi::AiModeration::SpamScanner.after_cooked_post(post)
- end
-
- plugin.on(:site_setting_changed) do |name, _old_value, new_value|
- if name == :ai_spam_detection_enabled && new_value
- ::DiscourseAi::AiModeration::SpamScanner.ensure_flagging_user!
- end
- end
-
- custom_filter = [
- :ai_spam_false_negative,
- Proc.new do |results, value|
- if value
- results.where(<<~SQL)
- EXISTS (
- SELECT 1 FROM ai_spam_logs
- WHERE NOT is_spam
- AND post_id = target_id AND target_type = 'Post'
- )
- SQL
- else
- results
- end
- end,
- ]
-
- Reviewable.add_custom_filter(custom_filter)
- end
- end
- end
-end
diff --git a/lib/ai_moderation/spam_metric.rb b/lib/ai_moderation/spam_metric.rb
deleted file mode 100644
index 063462fa..00000000
--- a/lib/ai_moderation/spam_metric.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiModeration
- class SpamMetric
- def self.update(new_status, reviewable)
- return if !defined?(::DiscoursePrometheus)
- ai_spam_log = AiSpamLog.find_by(reviewable:)
- return if ai_spam_log.nil?
-
- increment("scanned")
- increment("is_spam") if new_status == :approved && ai_spam_log.is_spam
- increment("false_positive") if new_status == :rejected && ai_spam_log.is_spam
- increment("false_negative") if new_status == :rejected && !ai_spam_log.is_spam
- end
-
- private
-
- def self.increment(type, value = 1)
- metric = ::DiscoursePrometheus::InternalMetric::Custom.new
- metric.name = "discourse_ai_spam_detection"
- metric.type = "Counter"
- metric.description = "AI spam scanning statistics"
- metric.labels = { db: RailsMultisite::ConnectionManagement.current_db, type: }
- metric.value = value
- $prometheus_client.send_json(metric.to_h)
- end
- end
- end
-end
diff --git a/lib/ai_moderation/spam_report.rb b/lib/ai_moderation/spam_report.rb
deleted file mode 100644
index f4574c19..00000000
--- a/lib/ai_moderation/spam_report.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiModeration
- class SpamReport
- def self.generate(min_date: 1.week.ago)
- spam_status = [Reviewable.statuses[:approved], Reviewable.statuses[:deleted]]
- ham_status = [Reviewable.statuses[:rejected], Reviewable.statuses[:ignored]]
-
- sql = <<~SQL
- WITH spam_stats AS (
- SELECT
- asl.reviewable_id,
- asl.post_id,
- asl.is_spam,
- r.status as reviewable_status,
- CASE WHEN EXISTS (
- SELECT 1 FROM reviewable_scores rs
- JOIN reviewables r1 ON r1.id = rs.reviewable_id
- WHERE r1.target_id = asl.post_id
- AND r1.target_type = 'Post'
- AND rs.reviewable_score_type = :spam_score_type
- AND NOT is_spam
- AND r1.status IN (:spam)
- ) THEN true ELSE false END AS missed_spam
- FROM ai_spam_logs asl
- LEFT JOIN reviewables r ON r.id = asl.reviewable_id
- WHERE asl.created_at > :min_date
- )
- SELECT
- COUNT(*) AS scanned_count,
- SUM(CASE WHEN is_spam THEN 1 ELSE 0 END) AS spam_detected,
- COUNT(CASE WHEN reviewable_status IN (:ham) THEN 1 END) AS false_positives,
- COUNT(CASE WHEN missed_spam THEN 1 END) AS false_negatives
- FROM spam_stats
- SQL
-
- DB.query(
- sql,
- spam: spam_status,
- ham: ham_status,
- min_date: min_date,
- spam_score_type: ReviewableScore.types[:spam],
- ).first
- end
- end
- end
-end
diff --git a/lib/ai_moderation/spam_scanner.rb b/lib/ai_moderation/spam_scanner.rb
deleted file mode 100644
index de469f9e..00000000
--- a/lib/ai_moderation/spam_scanner.rb
+++ /dev/null
@@ -1,466 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module AiModeration
- class SpamScanner
- POSTS_TO_SCAN = 3
- MINIMUM_EDIT_DIFFERENCE = 10
- EDIT_DELAY_MINUTES = 10
- MAX_AGE_TO_SCAN = 1.day
- MAX_RAW_SCAN_LENGTH = 5000
-
- SHOULD_SCAN_POST_CUSTOM_FIELD = "discourse_ai_should_scan_post"
-
- def self.new_post(post)
- return if !enabled?
- return if !should_scan_post?(post)
-
- flag_post_for_scanning(post)
- end
-
- def self.ensure_flagging_user!
- if !SiteSetting.ai_spam_detection_user_id.present?
- User.transaction do
- # prefer a "high" id for this bot
- id = User.where("id > -20").minimum(:id) - 1
- id = User.minimum(:id) - 1 if id == -100
-
- user =
- User.create!(
- id: id,
- username: UserNameSuggester.suggest("discourse_ai_spam"),
- name: "Discourse AI Spam Scanner",
- email: "#{SecureRandom.hex(10)}@invalid.invalid",
- active: true,
- approved: true,
- trust_level: TrustLevel[4],
- admin: true,
- )
- Group.user_trust_level_change!(user.id, user.trust_level)
-
- SiteSetting.ai_spam_detection_user_id = user.id
- end
- end
- end
-
- def self.flagging_user
- user = nil
- if SiteSetting.ai_spam_detection_user_id.present?
- user = User.find_by(id: SiteSetting.ai_spam_detection_user_id)
- ensure_safe_flagging_user!(user)
- end
- user || Discourse.system_user
- end
-
- def self.ensure_safe_flagging_user!(user)
- # only do repair on bot users, if somehow it is set to a human skip repairs
- return if !user.bot?
- user.update!(silenced_till: nil) if user.silenced?
- user.update!(trust_level: TrustLevel[4]) if user.trust_level != TrustLevel[4]
- user.update!(suspended_till: nil, suspended_at: nil) if user.suspended?
- user.update!(active: true) if !user.active?
- end
-
- def self.after_cooked_post(post)
- return if !enabled?
- return if !should_scan_post?(post)
- return if !post.custom_fields[SHOULD_SCAN_POST_CUSTOM_FIELD]
- return if post.updated_at < MAX_AGE_TO_SCAN.ago
-
- last_scan = AiSpamLog.where(post_id: post.id).order(created_at: :desc).first
-
- if last_scan && last_scan.created_at > EDIT_DELAY_MINUTES.minutes.ago
- delay_minutes =
- ((last_scan.created_at + EDIT_DELAY_MINUTES.minutes) - Time.current).to_i / 60
- Jobs.enqueue_in(delay_minutes.minutes, :ai_spam_scan, post_id: post.id)
- else
- Jobs.enqueue(:ai_spam_scan, post_id: post.id)
- end
- end
-
- def self.edited_post(post)
- return if !enabled?
- return if !should_scan_post?(post)
- return if scanned_max_times?(post)
-
- previous_version = post.revisions.last&.modifications&.dig("raw", 0)
- current_version = post.raw
-
- return if !significant_change?(previous_version, current_version)
-
- flag_post_for_scanning(post)
- end
-
- def self.flag_post_for_scanning(post)
- post.custom_fields[SHOULD_SCAN_POST_CUSTOM_FIELD] = "true"
- post.save_custom_fields
- end
-
- def self.enabled?
- SiteSetting.ai_spam_detection_enabled && SiteSetting.discourse_ai_enabled
- end
-
- def self.should_scan_post?(post)
- return false if !post.present?
- return false if post.user.trust_level > TrustLevel[1]
- return false if post.topic.private_message?
- return false if post.user.bot?
- return false if post.user.staff?
-
- if Post
- .where(user_id: post.user_id)
- .joins(:topic)
- .where(topic: { archetype: Archetype.default })
- .limit(4)
- .count > 3
- return false
- end
- true
- end
-
- def self.scanned_max_times?(post)
- AiSpamLog.where(post_id: post.id).count >= 3
- end
-
- def self.significant_change?(previous_version, current_version)
- return true if previous_version.nil? # First edit should be scanned
-
- # Use Discourse's built-in levenshtein implementation
- distance =
- ScreenedEmail.levenshtein(previous_version.to_s[0...1000], current_version.to_s[0...1000])
-
- distance >= MINIMUM_EDIT_DIFFERENCE
- end
-
- def self.test_post(post, custom_instructions: nil, llm_id: nil)
- settings = AiModerationSetting.spam
- custom_instructions = custom_instructions || settings.custom_instructions.presence
-
- target_msg =
- build_target_content_msg(
- post,
- post.topic || Topic.with_deleted.find_by(id: post.topic_id),
- )
- custom_insts = custom_instructions || settings.custom_instructions.presence
- if custom_insts.present?
- custom_insts =
- "\n\nAdditional site-specific instructions provided by Staff:\n#{custom_insts}"
- end
-
- ctx =
- build_bot_context(
- feature_name: "spam_detection_test",
- messages: [target_msg],
- custom_instructions: custom_insts,
- )
- bot = build_scanner_bot(settings: settings, llm_id: llm_id)
-
- structured_output = nil
- llm_args = { feature_context: { post_id: post.id } }
- bot.reply(ctx, llm_args: llm_args) do |partial, _, type|
- structured_output = partial if type == :structured_output
- end
-
- history = nil
- AiSpamLog
- .where(post: post)
- .order(:created_at)
- .limit(100)
- .each do |log|
- history ||= +"Scan History:\n"
- history << "date: #{log.created_at} is_spam: #{log.is_spam}\n"
- end
-
- log = +"Scanning #{post.url}\n\n"
-
- if history
- log << history
- log << "\n"
- end
-
- used_llm = bot.model
- log << "LLM: #{used_llm.name}\n\n"
-
- spam_persona = bot.persona
- used_prompt = spam_persona.craft_prompt(ctx, llm: used_llm).system_message_text
- log << "System Prompt: #{used_prompt}\n\n"
-
- text_content =
- if target_msg[:content].is_a?(Array)
- target_msg[:content].first
- else
- target_msg[:content]
- end
-
- log << "Context: #{text_content}\n\n"
-
- is_spam = is_spam?(structured_output)
-
- reasoning_insts = {
- type: :user,
- content: "Don't return a JSON this time. Explain your reasoning in plain text.",
- }
- ctx.messages = [
- target_msg,
- { type: :model, content: { spam: is_spam }.to_json },
- reasoning_insts,
- ]
- ctx.bypass_response_format = true
-
- reasoning = +""
-
- bot.reply(ctx, llm_args: llm_args.merge(max_tokens: 100)) do |partial, _, type|
- reasoning << partial if type.blank?
- end
-
- log << "#{reasoning.strip}"
-
- { is_spam: is_spam, log: log }
- end
-
- def self.perform_scan(post)
- return if !should_scan_post?(post)
-
- perform_scan!(post)
- end
-
- def self.perform_scan!(post)
- return if !enabled?
- settings = AiModerationSetting.spam
- return if !settings || !settings.llm_model || !settings.ai_persona
-
- target_msg = build_target_content_msg(post)
- custom_instructions = settings.custom_instructions.presence
- if custom_instructions.present?
- custom_instructions =
- "\n\nAdditional site-specific instructions provided by Staff:\n#{custom_instructions}"
- end
-
- ctx =
- build_bot_context(
- messages: [target_msg],
- custom_instructions: custom_instructions,
- user: self.flagging_user,
- )
- bot = build_scanner_bot(settings: settings, user: self.flagging_user)
- structured_output = nil
-
- begin
- llm_args = { feature_context: { post_id: post.id } }
- bot.reply(ctx, llm_args: llm_args) do |partial, _, type|
- structured_output = partial if type == :structured_output
- end
-
- is_spam = is_spam?(structured_output)
-
- log = AiApiAuditLog.order(id: :desc).where(feature_name: "spam_detection").first
- text_content =
- if target_msg[:content].is_a?(Array)
- target_msg[:content].first
- else
- target_msg[:content]
- end
- AiSpamLog.transaction do
- log =
- AiSpamLog.create!(
- post: post,
- llm_model: settings.llm_model,
- ai_api_audit_log: log,
- is_spam: is_spam,
- payload: text_content,
- )
- handle_spam(post, log) if is_spam
- end
- rescue StandardError => e
- # we need retries otherwise stuff will not be handled
- Discourse.warn_exception(
- e,
- message: "Discourse AI: Error in SpamScanner for post #{post.id}",
- )
- raise e
- end
- end
-
- def self.fix_spam_scanner_not_admin
- user = DiscourseAi::AiModeration::SpamScanner.flagging_user
-
- if user.present?
- user.update!(admin: true)
- else
- raise Discourse::NotFound
- end
- end
-
- private
-
- def self.build_bot_context(
- feature_name: "spam_detection",
- messages:,
- custom_instructions: nil,
- bypass_response_format: false,
- user: Discourse.system_user
- )
- DiscourseAi::Personas::BotContext
- .new(
- user: user,
- skip_tool_details: true,
- feature_name: feature_name,
- messages: messages,
- bypass_response_format: bypass_response_format,
- )
- .tap { |ctx| ctx.custom_instructions = custom_instructions if custom_instructions }
- end
-
- def self.build_scanner_bot(
- settings:,
- use_structured_output: true,
- llm_id: nil,
- user: Discourse.system_user
- )
- persona = settings.ai_persona.class_instance&.new
-
- llm_model = llm_id ? LlmModel.find(llm_id) : settings.llm_model
-
- DiscourseAi::Personas::Bot.as(user, persona: persona, model: llm_model)
- end
-
- def self.is_spam?(structured_output)
- structured_output.present? && structured_output.read_buffered_property(:spam)
- end
-
- def self.build_target_content_msg(post, topic = nil)
- topic ||= post.topic
- context = []
-
- # Clear distinction between reply and new topic
- if post.is_first_post?
- context << "NEW TOPIC POST ANALYSIS"
- context << "- Topic title: #{topic.title}"
- context << "- Category: #{topic.category&.name}"
- else
- context << "REPLY POST ANALYSIS"
- context << "- In topic: #{topic.title}"
- context << "- Category: #{topic.category&.name}"
- context << "- Topic started by: #{topic.user&.username}"
-
- if post.reply_to_post_number.present?
- parent =
- Post.with_deleted.find_by(topic_id: topic.id, post_number: post.reply_to_post_number)
- if parent
- context << "\nReplying to #{parent.user&.username}'s post:"
- context << "#{parent.raw[0..500]}..." if parent.raw.length > 500
- context << parent.raw if parent.raw.length <= 500
- end
- end
- end
-
- context << "\nPost Author Information:"
- if user = post.user # during test we may not have a user
- context << "- Username: #{user.username}\n"
- context << "- Email: #{user.email}\n"
- context << "- Account age: #{(Time.current - user.created_at).to_i / 86_400} days\n"
- context << "- Total posts: #{user.post_count}\n"
- context << "- Trust level: #{user.trust_level}\n"
- if info = location_info(user)
- context << "- Registration Location: #{info[:registration]}\n" if info[:registration]
- context << "- Last Location: #{info[:last]}\n" if info[:last]
- end
- end
-
- context << "\nPost Content (first #{MAX_RAW_SCAN_LENGTH} chars):\n"
- context << post.raw[0..MAX_RAW_SCAN_LENGTH]
-
- user_msg = { type: :user, content: context.join("\n") }
-
- upload_ids = post.upload_ids
- if upload_ids.present?
- user_msg[:content] = [user_msg[:content]]
- upload_ids.take(3).each { |upload_id| user_msg[:content] << { upload_id: upload_id } }
- end
-
- user_msg
- end
-
- def self.location_info(user)
- registration, last = nil
- if user.ip_address.present?
- info = DiscourseIpInfo.get(user.ip_address, resolve_hostname: true)
- last = "#{info[:location]} (#{info[:organization]})" if info && info[:location].present?
- end
- if user.registration_ip_address.present?
- info = DiscourseIpInfo.get(user.registration_ip_address, resolve_hostname: true)
- registration = "#{info[:location]} (#{info[:organization]})" if info &&
- info[:location].present?
- end
-
- rval = nil
- if registration || last
- rval = { registration: registration } if registration
- if last && last != registration
- rval ||= {}
- rval[:last] = last
- end
- end
-
- rval
- rescue => e
- Discourse.warn_exception(e, message: "Failed to lookup location info")
- nil
- end
-
- def self.handle_spam(post, log)
- url = "#{Discourse.base_url}/admin/plugins/discourse-ai/ai-spam"
- reason = I18n.t("discourse_ai.spam_detection.flag_reason", url: url)
-
- flagging_user = self.flagging_user
-
- result =
- PostActionCreator.new(
- flagging_user,
- post,
- PostActionType.types[:spam],
- reason: reason,
- queue_for_review: true,
- ).perform
-
- # Currently in core re-flagging something that is already flagged as spam
- # is not supported, long term we may want to support this but in the meantime
- # we should not be silencing/hiding if the PostActionCreator fails.
- if result.success?
- log.update!(reviewable: result.reviewable)
-
- reason = I18n.t("discourse_ai.spam_detection.silence_reason", url: url)
- silencer =
- UserSilencer.new(
- post.user,
- flagging_user,
- message: :too_many_spam_flags,
- post_id: post.id,
- reason: reason,
- keep_posts: true,
- )
- silencer.silence
-
- # silencer will not hide tl1 posts, so we do this here
- hide_post(post)
- else
- log.update!(
- error:
- "unable to flag post as spam, post action failed for post #{post.id} with error: '#{result.errors.full_messages.join(", ").truncate(3000)}'",
- )
- end
- end
-
- def self.hide_post(post)
- Post.where(id: post.id).update_all(
- [
- "hidden = true, hidden_reason_id = COALESCE(hidden_reason_id, ?)",
- Post.hidden_reasons[:new_user_spam_threshold_reached],
- ],
- )
-
- Topic.where(id: post.topic_id).update_all(visible: false) if post.post_number == 1
- end
- end
- end
-end
diff --git a/lib/automation.rb b/lib/automation.rb
deleted file mode 100644
index 0410aed1..00000000
--- a/lib/automation.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Automation
- def self.flag_types
- [
- { id: "review", translated_name: I18n.t("discourse_automation.ai.flag_types.review") },
- {
- id: "review_hide",
- translated_name: I18n.t("discourse_automation.ai.flag_types.review_hide"),
- },
- { id: "spam", translated_name: I18n.t("discourse_automation.ai.flag_types.spam") },
- {
- id: "spam_silence",
- translated_name: I18n.t("discourse_automation.ai.flag_types.spam_silence"),
- },
- ]
- end
-
- def self.available_custom_tools
- AiTool
- .where(enabled: true)
- .where("parameters = '[]'::jsonb")
- .pluck(:id, :name, :description)
- .map { |id, name, description| { id: id, translated_name: name, description: description } }
- end
-
- def self.available_models
- values = DB.query_hash(<<~SQL)
- SELECT display_name AS translated_name, id AS id
- FROM llm_models
- SQL
-
- values =
- values
- .filter do |value_h|
- value_h["id"] > 0 ||
- SiteSetting.ai_automation_allowed_seeded_models_map.include?(value_h["id"].to_s)
- end
- .each { |value_h| value_h["id"] = "custom:#{value_h["id"]}" }
-
- values
- end
-
- def self.available_persona_choices(require_user: true, require_default_llm: true)
- relation = AiPersona.includes(:user)
- relation = relation.where.not(user_id: nil) if require_user
- relation = relation.where.not(default_llm: nil) if require_default_llm
- relation.map do |persona|
- phash = { id: persona.id, translated_name: persona.name, description: persona.name }
-
- phash[:description] += " (#{persona&.user&.username})" if require_user
-
- phash
- end
- end
- end
-end
diff --git a/lib/automation/llm_persona_triage.rb b/lib/automation/llm_persona_triage.rb
deleted file mode 100644
index dbbdbc6a..00000000
--- a/lib/automation/llm_persona_triage.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi
- module Automation
- module LlmPersonaTriage
- def self.handle(post:, persona_id:, whisper: false, silent_mode: false, automation: nil)
- DiscourseAi::AiBot::Playground.reply_to_post(
- post: post,
- persona_id: persona_id,
- whisper: whisper,
- silent_mode: silent_mode,
- feature_name: "automation - #{automation&.name}",
- )
- rescue => e
- Discourse.warn_exception(
- e,
- message: "Error responding to: #{post&.url} in LlmPersonaTriage.handle",
- )
- raise e if Rails.env.test?
- nil
- end
- end
- end
-end
diff --git a/lib/automation/llm_tool_triage.rb b/lib/automation/llm_tool_triage.rb
deleted file mode 100644
index 58b9210d..00000000
--- a/lib/automation/llm_tool_triage.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi
- module Automation
- module LlmToolTriage
- def self.handle(post:, tool_id:, automation: nil)
- tool = AiTool.find_by(id: tool_id)
- return if !tool
- return if !tool.parameters.blank?
-
- context = DiscourseAi::Personas::BotContext.new(post: post)
-
- runner = tool.runner({}, llm: nil, bot_user: Discourse.system_user, context: context)
- runner.invoke
- end
- end
- end
-end
diff --git a/lib/automation/llm_triage.rb b/lib/automation/llm_triage.rb
deleted file mode 100644
index 93f06712..00000000
--- a/lib/automation/llm_triage.rb
+++ /dev/null
@@ -1,168 +0,0 @@
-# frozen_string_literal: true
-#
-module DiscourseAi
- module Automation
- module LlmTriage
- def self.handle(
- post:,
- model:,
- search_for_text:,
- system_prompt:,
- category_id: nil,
- tags: nil,
- canned_reply: nil,
- canned_reply_user: nil,
- hide_topic: nil,
- flag_post: nil,
- flag_type: nil,
- automation: nil,
- max_post_tokens: nil,
- stop_sequences: nil,
- temperature: nil,
- whisper: nil,
- reply_persona_id: nil,
- max_output_tokens: nil,
- action: nil
- )
- if category_id.blank? && tags.blank? && canned_reply.blank? && hide_topic.blank? &&
- flag_post.blank? && reply_persona_id.blank?
- raise ArgumentError, "llm_triage: no action specified!"
- end
-
- if action == :edit && category_id.blank? && tags.blank? && flag_post.blank? &&
- hide_topic.blank?
- return
- end
-
- llm = DiscourseAi::Completions::Llm.proxy(model)
-
- s_prompt = system_prompt.to_s.sub("%%POST%%", "") # Backwards-compat. We no longer sub this.
- prompt = DiscourseAi::Completions::Prompt.new(s_prompt)
-
- content = "title: #{post.topic.title}\n#{post.raw}"
-
- content =
- llm.tokenizer.truncate(
- content,
- max_post_tokens,
- strict: SiteSetting.ai_strict_token_counting,
- ) if max_post_tokens.present?
-
- if post.upload_ids.present?
- content = [content]
- content.concat(post.upload_ids.map { |upload_id| { upload_id: upload_id } })
- end
-
- prompt.push(type: :user, content: content)
-
- result = nil
-
- result =
- llm.generate(
- prompt,
- max_tokens: max_output_tokens,
- temperature: temperature,
- user: Discourse.system_user,
- stop_sequences: stop_sequences,
- feature_name: "llm_triage",
- feature_context: {
- automation_id: automation&.id,
- automation_name: automation&.name,
- },
- )&.strip
-
- if result.present? && result.downcase.include?(search_for_text.downcase)
- user = User.find_by_username(canned_reply_user) if canned_reply_user.present?
- original_user = user
- user = user || Discourse.system_user
- if reply_persona_id.present? && action != :edit
- begin
- DiscourseAi::AiBot::Playground.reply_to_post(
- post: post,
- persona_id: reply_persona_id,
- whisper: whisper,
- user: original_user,
- )
- rescue StandardError => e
- Discourse.warn_exception(
- e,
- message: "Error responding to: #{post&.url} in LlmTriage.handle",
- )
- raise e if Rails.env.test?
- end
- elsif canned_reply.present? && action != :edit
- post_type = whisper ? Post.types[:whisper] : Post.types[:regular]
- PostCreator.create!(
- user,
- topic_id: post.topic_id,
- raw: canned_reply,
- reply_to_post_number: post.post_number,
- skip_validations: true,
- post_type: post_type,
- )
- end
-
- changes = {}
- changes[:category_id] = category_id if category_id.present?
- if SiteSetting.tagging_enabled? && tags.present?
- changes[:tags] = post.topic.tags.map(&:name).concat(tags)
- end
-
- if changes.present?
- first_post = post.topic.posts.where(post_number: 1).first
- changes[:bypass_bump] = true
- changes[:skip_validations] = true
- first_post.revise(Discourse.system_user, changes)
- end
-
- post.topic.update!(visible: false) if hide_topic
-
- if flag_post
- score_reason =
- I18n
- .t("discourse_automation.scriptables.llm_triage.flagged_post")
- .sub("%%LLM_RESPONSE%%", result)
- .sub("%%AUTOMATION_ID%%", automation&.id.to_s)
- .sub("%%AUTOMATION_NAME%%", automation&.name.to_s)
-
- if flag_type == :spam || flag_type == :spam_silence
- result =
- PostActionCreator.new(
- Discourse.system_user,
- post,
- PostActionType.types[:spam],
- message: score_reason,
- queue_for_review: true,
- ).perform
-
- if flag_type == :spam_silence
- if result.success?
- SpamRule::AutoSilence.new(post.user, post).silence_user
- else
- Rails.logger.warn(
- "llm_triage: unable to flag post as spam, post action failed for #{post.id} with error: '#{result.errors.full_messages.join(",").truncate(3000)}'",
- )
- end
- end
- else
- reviewable =
- ReviewablePost.needs_review!(target: post, created_by: Discourse.system_user)
-
- reviewable.add_score(
- Discourse.system_user,
- ReviewableScore.types[:needs_approval],
- reason: score_reason,
- force_review: true,
- )
-
- # We cannot do this through the PostActionCreator because hiding a post is reserved for auto action flags.
- # Those flags are off_topic, inappropiate, and spam. We want a more generic type for triage, so none of those
- # fit here.
- post.hide!(PostActionType.types[:notify_moderators]) if flag_type == :review_hide
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/automation/report_context_generator.rb b/lib/automation/report_context_generator.rb
deleted file mode 100644
index e54bf235..00000000
--- a/lib/automation/report_context_generator.rb
+++ /dev/null
@@ -1,246 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Automation
- class ReportContextGenerator
- def self.generate(**args)
- new(**args).generate
- end
-
- def initialize(
- start_date:,
- duration:,
- category_ids: nil,
- tags: nil,
- allow_secure_categories: false,
- max_posts: 200,
- tokens_per_post: 100,
- tokenizer: nil,
- prioritized_group_ids: [],
- exclude_category_ids: nil,
- exclude_tags: nil
- )
- @start_date = start_date
- @duration = duration
- @category_ids = category_ids
- @tags = tags
- @allow_secure_categories = allow_secure_categories
- @max_posts = max_posts
- @tokenizer = tokenizer || DiscourseAi::Tokenizer::OpenAiTokenizer
- @tokens_per_post = tokens_per_post
- @prioritized_group_ids = prioritized_group_ids
-
- @posts =
- Post
- .where("posts.created_at >= ?", @start_date)
- .joins(topic: :category)
- .includes(:topic, :user)
- .where("topics.visible")
- .where("posts.created_at < ?", @start_date + @duration)
- .where("posts.post_type = ?", Post.types[:regular])
- .where("posts.hidden_at IS NULL")
- .where("topics.deleted_at IS NULL")
- .where("topics.archetype = ?", Archetype.default)
- @posts = @posts.where("categories.read_restricted = ?", false) if !@allow_secure_categories
- @posts = @posts.where("categories.id IN (?)", @category_ids) if @category_ids.present?
- @posts =
- @posts.where(
- "categories.id NOT IN (:ids) AND
- (parent_category_id NOT IN (:ids) OR parent_category_id IS NULL)",
- ids: exclude_category_ids,
- ) if exclude_category_ids.present?
-
- if exclude_tags.present?
- exclude_tag_ids = Tag.where_name(exclude_tags).select(:id)
- @posts =
- @posts.where(
- "topics.id NOT IN (?)",
- TopicTag.where(tag_id: exclude_tag_ids).select(:topic_id),
- )
- end
-
- if @tags.present?
- tag_ids = Tag.where_name(@tags).select(:id)
- topic_ids_with_tags = TopicTag.where(tag_id: tag_ids).select(:topic_id)
- @posts = @posts.where(topic_id: topic_ids_with_tags)
- end
-
- if defined?(::DiscourseSolved)
- @solutions =
- DiscourseSolved::SolvedTopic
- .where(topic_id: @posts.select(:topic_id))
- .pluck(:topic_id, :answer_post_id)
- .to_h
- else
- @solutions = {}
- end
- end
-
- def format_topic(topic)
- info = []
- info << ""
- info << "### #{topic.title}"
- info << "topic_id: #{topic.id}"
- info << "solved: true" if @solutions.key?(topic.id)
- info << "category: #{topic.category&.name}"
- # We may make this optional, but for now we remove all
- # tags that are not visible to anon
- tags = topic.tags.visible(Guardian.new).pluck(:name)
- info << "tags: #{tags.join(", ")}" if tags.present?
- info << topic.created_at.strftime("%Y-%m-%d %H:%M")
- { created_at: topic.created_at, info: info.join("\n"), posts: {} }
- end
-
- def format_post(post)
- buffer = []
- buffer << ""
- buffer << "post_number: #{post.post_number}"
- buffer << "solution: true" if @solutions[post.topic_id] == post.id
- buffer << post.created_at.strftime("%Y-%m-%d %H:%M")
- buffer << "user: #{post.user&.username}"
- buffer << "likes: #{post.like_count}"
- excerpt =
- @tokenizer.truncate(
- post.raw,
- @tokens_per_post,
- strict: SiteSetting.ai_strict_token_counting,
- )
- excerpt = "excerpt: #{excerpt}..." if excerpt.length < post.raw.length
- buffer << "#{excerpt}"
- { likes: post.like_count, info: buffer.join("\n") }
- end
-
- def format_summary
- topic_count =
- @posts
- .where("topics.created_at > ?", @start_date)
- .select(:topic_id)
- .distinct(:topic_id)
- .count
-
- buffer = []
- buffer << "Start Date: #{@start_date.to_date}"
- buffer << "End Date: #{(@start_date + @duration).to_date}"
- buffer << "New posts: #{@posts.count}"
- buffer << "New topics: #{topic_count}"
-
- top_users =
- Post
- .where(id: @posts.select(:id))
- .joins(:user)
- .group(:user_id, :username)
- .select(
- "user_id, username, sum(posts.like_count) like_count, count(posts.id) post_count",
- )
- .order("sum(posts.like_count) desc")
- .limit(10)
-
- buffer << "Top users:"
- top_users.each do |user|
- buffer << "@#{user.username} (#{user.like_count} likes, #{user.post_count} posts)"
- end
-
- if @prioritized_group_ids.present?
- group_names =
- Group
- .where(id: @prioritized_group_ids)
- .pluck(:name, :full_name)
- .map do |name, full_name|
- if full_name.present?
- "#{name} (#{full_name[0..100].gsub("\n", " ")})"
- else
- name
- end
- end
- .join(", ")
- buffer << ""
- buffer << "Top users in #{group_names} group#{group_names.include?(",") ? "s" : ""}:"
-
- group_users = GroupUser.where(group_id: @prioritized_group_ids).select(:user_id)
- top_users
- .where(user_id: group_users)
- .each do |user|
- buffer << "@#{user.username} (#{user.like_count} likes, #{user.post_count} posts)"
- end
- end
-
- buffer.join("\n")
- end
-
- def format_topics
- buffer = []
- topics = {}
-
- post_count = 0
-
- @posts = @posts.order("posts.like_count desc, posts.created_at desc")
-
- if @prioritized_group_ids.present?
- user_groups = GroupUser.where(group_id: @prioritized_group_ids)
- prioritized_posts = @posts.where(user_id: user_groups.select(:user_id)).limit(@max_posts)
-
- post_count += add_posts(prioritized_posts, topics)
- end
-
- add_posts(@posts.limit(@max_posts), topics, limit: @max_posts - post_count)
-
- # we need last posts in all topics
- # they may have important info
- last_posts =
- @posts.where("posts.post_number = topics.highest_post_number").where(
- "topics.id IN (?)",
- topics.keys,
- )
-
- add_posts(last_posts, topics)
-
- topics.each do |topic_id, topic_info|
- topic_info[:post_likes] = topic_info[:posts].sum { |_, post_info| post_info[:likes] }
- end
-
- topics = topics.sort { |a, b| b[1][:post_likes] <=> a[1][:post_likes] }
-
- topics.each do |topic_id, topic_info|
- buffer << topic_info[:info]
-
- last_post_number = 0
-
- topic_info[:posts]
- .sort { |a, b| a[0] <=> b[0] }
- .each do |post_number, post_info|
- buffer << "\n..." if post_number > last_post_number + 1
- buffer << post_info[:info]
- last_post_number = post_number
- end
- end
-
- buffer.join("\n")
- end
-
- def generate
- buffer = []
-
- buffer << "## Summary"
- buffer << format_summary
- buffer << "\n## Topics"
- buffer << format_topics
-
- buffer.join("\n")
- end
-
- def add_posts(relation, topics, limit: nil)
- post_count = 0
- relation.each do |post|
- topics[post.topic_id] ||= format_topic(post.topic)
- if !topics[post.topic_id][:posts][post.post_number]
- topics[post.topic_id][:posts][post.post_number] = format_post(post)
- post_count += 1
- limit -= 1 if limit
- end
- break if limit && limit <= 0
- end
- post_count
- end
- end
- end
-end
diff --git a/lib/automation/report_runner.rb b/lib/automation/report_runner.rb
deleted file mode 100644
index 1c207a45..00000000
--- a/lib/automation/report_runner.rb
+++ /dev/null
@@ -1,270 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Automation
- class ReportRunner
- def self.default_instructions
- # not localizing for now cause non English LLM will require
- # a fair bit of experimentation
- <<~TEXT
- Generate report:
-
- ## Report Guidelines:
-
- - Length & Style: Aim for 12 dense paragraphs in a narrative style, focusing on internal forum discussions.
- - Accuracy: Only include verified information with no embellishments.
- - Sourcing: ALWAYS Back statements with links to forum discussions.
- - Markdown Usage: Enhance readability with **bold**, *italic*, and > quotes.
- - Linking: Use `#{Discourse.base_url}/t/-/TOPIC_ID/POST_NUMBER` for direct references.
- - User Mentions: Reference users with @USERNAME
- - Add many topic links: strive to link to at least 30 topics in the report. Topic Id is meaningless to end users if you need to throw in a link use [ref](...) or better still just embed it into the [sentence](...)
- - Categories and tags: use the format #TAG and #CATEGORY to denote tags and categories
-
- ## Structure:
-
- - Key statistics: Specify date range, call out important stats like number of new topics and posts
- - Overview: Briefly state trends within period.
- - Highlighted content: 5 paragraphs highlighting important topics people should know about. If possible have each paragraph link to multiple related topics.
- - Key insights and trends linking to a selection of posts that back them
- TEXT
- end
-
- def self.run!(**args)
- new(**args).run!
- end
-
- def initialize(
- sender_username:,
- model:,
- persona_id:,
- sample_size:,
- instructions:,
- tokens_per_post:,
- days:,
- offset:,
- receivers: nil,
- topic_id: nil,
- title: nil,
- category_ids: nil,
- tags: nil,
- priority_group_id: nil,
- allow_secure_categories: false,
- debug_mode: false,
- exclude_category_ids: nil,
- exclude_tags: nil,
- top_p: 0.1,
- temperature: 0.2,
- suppress_notifications: false,
- automation: nil
- )
- @sender = User.find_by(username: sender_username)
- receivers_without_emails = receivers&.reject { |r| r.include? "@" }
- if receivers_without_emails.present?
- @group_receivers = Group.where(name: receivers_without_emails)
- receivers_without_emails -= @group_receivers.pluck(:name)
- @receivers = User.where(username: receivers_without_emails)
- else
- @group_receivers = []
- @receivers = []
- end
- @email_receivers = receivers&.filter { |r| r.include? "@" }
- @title =
- if title.present?
- title
- else
- I18n.t("discourse_automation.scriptables.llm_report.title")
- end
- @model = LlmModel.find_by(id: model.split(":")&.last)
- @persona = AiPersona.find(persona_id).class_instance.new
- @category_ids = category_ids
- @tags = tags
- @allow_secure_categories = allow_secure_categories
- @debug_mode = debug_mode
- @sample_size = sample_size.to_i < 10 ? 10 : sample_size.to_i
- @instructions = instructions
- @days = days.to_i
- @offset = offset.to_i
- @priority_group_id = priority_group_id
- @tokens_per_post = tokens_per_post.to_i
- @topic_id = topic_id.presence&.to_i
- @exclude_category_ids = exclude_category_ids
- @exclude_tags = exclude_tags
-
- @top_p = top_p
- @temperature = temperature
-
- @top_p = nil if top_p.to_f < 0
- @temperature = nil if temperature.to_f < 0
- @suppress_notifications = suppress_notifications
-
- if !@topic_id && !@receivers.present? && !@group_receivers.present? &&
- !@email_receivers.present?
- raise ArgumentError, "Must specify topic_id or receivers"
- end
- @automation = automation
- end
-
- def run!
- start_date = (@offset + @days).days.ago
- end_date = start_date + @days.days
-
- title =
- @title.gsub(
- "%DATE%",
- start_date.strftime("%Y-%m-%d") + " - " + end_date.strftime("%Y-%m-%d"),
- )
-
- prioritized_group_ids = [@priority_group_id] if @priority_group_id.present?
- context =
- DiscourseAi::Automation::ReportContextGenerator.generate(
- start_date: start_date,
- duration: @days.days,
- max_posts: @sample_size,
- tags: @tags,
- category_ids: @category_ids,
- prioritized_group_ids: prioritized_group_ids,
- allow_secure_categories: @allow_secure_categories,
- tokens_per_post: @tokens_per_post,
- tokenizer: @model.tokenizer_class,
- exclude_category_ids: @exclude_category_ids,
- exclude_tags: @exclude_tags,
- )
- input = <<~INPUT.strip
- #{@instructions}
-
- Real and accurate context from the Discourse forum is included in the tag below.
-
-
- #{context}
-
-
- #{@instructions}
- INPUT
-
- report_ctx =
- DiscourseAi::Personas::BotContext.new(
- user: Discourse.system_user,
- skip_tool_details: true,
- feature_name: "ai_report",
- messages: [{ type: :user, content: input }],
- )
-
- puts if Rails.env.development? && @debug_mode
-
- result = +""
- bot = DiscourseAi::Personas::Bot.as(Discourse.system_user, persona: @persona, model: @model)
- json_summary_schema_key = @persona.response_format&.first.to_h
-
- buffer_blk =
- Proc.new do |partial, _, type|
- if type == :structured_output
- read_chunk = partial.read_buffered_property(json_summary_schema_key["key"]&.to_sym)
-
- print read_chunk if Rails.env.development? && @debug_mode
- result << read_chunk if read_chunk.present?
- elsif type.blank?
- # Assume response is a regular completion.
- print partial if Rails.env.development? && @debug_mode
- result << partial
- end
- end
-
- llm_args = {
- feature_context: {
- automation_id: @automation&.id,
- automation_name: @automation&.name,
- },
- }
- bot.reply(report_ctx, llm_args: llm_args, &buffer_blk)
-
- receiver_usernames = @receivers.map(&:username).join(",")
- receiver_groupnames = @group_receivers.map(&:name).join(",")
-
- result = suppress_notifications(result) if @suppress_notifications
-
- if @topic_id
- PostCreator.create!(@sender, raw: result, topic_id: @topic_id, skip_validations: true)
- # no debug mode for topics, it is too noisy
- end
-
- if receiver_usernames.present? || receiver_groupnames.present?
- post =
- PostCreator.create!(
- @sender,
- raw: result,
- title: title,
- archetype: Archetype.private_message,
- target_usernames: receiver_usernames,
- target_group_names: receiver_groupnames,
- skip_validations: true,
- )
-
- if @debug_mode
- input = input.split("\n").map { |line| " #{line}" }.join("\n")
- raw = <<~RAW
- ```
- tokens: #{@model.tokenizer_class.tokenize(input).length}
- start_date: #{start_date},
- duration: #{@days.days},
- max_posts: #{@sample_size},
- tags: #{@tags},
- category_ids: #{@category_ids},
- priority_group: #{@priority_group_id}
- model: #{@model.display_name}
- temperature: #{@temperature}
- top_p: #{@top_p}
- LLM context was:
- ```
-
- #{input}
- RAW
- PostCreator.create!(@sender, raw: raw, topic_id: post.topic_id, skip_validations: true)
- end
- end
-
- if @email_receivers.present?
- @email_receivers.each do |to_address|
- Email::Sender.new(
- ::AiReportMailer.send_report(to_address, subject: title, body: result),
- :ai_report,
- ).send
- end
- end
- end
-
- private
-
- def suppress_notifications(raw)
- cooked = PrettyText.cook(raw, sanitize: false)
- parsed = Nokogiri::HTML5.fragment(cooked)
-
- parsed
- .css("a")
- .each do |a|
- if a["class"] == "mention"
- a.inner_html = a.inner_html.sub("@", "")
- next
- end
- href = a["href"]
- if href.present? && (href.start_with?("#{Discourse.base_url}") || href.start_with?("/"))
- begin
- uri = URI.parse(href)
- if uri.query.present?
- params = CGI.parse(uri.query)
- params["silent"] = "true"
- uri.query = URI.encode_www_form(params)
- else
- uri.query = "silent=true"
- end
- a["href"] = uri.to_s
- rescue URI::InvalidURIError
- # skip
- end
- end
- end
-
- parsed.to_html
- end
- end
- end
-end
diff --git a/lib/completions/anthropic_message_processor.rb b/lib/completions/anthropic_message_processor.rb
deleted file mode 100644
index a8ba9b4a..00000000
--- a/lib/completions/anthropic_message_processor.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-# frozen_string_literal: true
-
-class DiscourseAi::Completions::AnthropicMessageProcessor
- class AnthropicToolCall
- attr_reader :name, :raw_json, :id
-
- def initialize(name, id, partial_tool_calls: false)
- @name = name
- @id = id
- @raw_json = +""
- @tool_call = DiscourseAi::Completions::ToolCall.new(id: id, name: name, parameters: {})
- @streaming_parser =
- DiscourseAi::Completions::JsonStreamingTracker.new(self) if partial_tool_calls
- end
-
- def append(json)
- @raw_json << json
- @streaming_parser << json if @streaming_parser
- end
-
- def notify_progress(key, value)
- @tool_call.partial = true
- @tool_call.parameters[key.to_sym] = value
- @has_new_data = true
- end
-
- def has_partial?
- @has_new_data
- end
-
- def partial_tool_call
- @has_new_data = false
- @tool_call
- end
-
- def to_tool_call
- parameters = {}
- parameters = JSON.parse(raw_json, symbolize_names: true) if raw_json.present?
- # we dupe to avoid poisoning the original tool call
- @tool_call = @tool_call.dup
- @tool_call.partial = false
- @tool_call.parameters = parameters
- @tool_call
- end
- end
-
- attr_reader :tool_calls, :input_tokens, :output_tokens, :output_thinking
-
- def initialize(streaming_mode:, partial_tool_calls: false, output_thinking: false)
- @streaming_mode = streaming_mode
- @tool_calls = []
- @current_tool_call = nil
- @partial_tool_calls = partial_tool_calls
- @output_thinking = output_thinking
- @thinking = nil
- end
-
- def to_tool_calls
- @tool_calls.map { |tool_call| tool_call.to_tool_call }
- end
-
- def process_streamed_message(parsed)
- result = nil
- if parsed[:type] == "content_block_start" && parsed.dig(:content_block, :type) == "tool_use"
- tool_name = parsed.dig(:content_block, :name)
- tool_id = parsed.dig(:content_block, :id)
- result = @current_tool_call.to_tool_call if @current_tool_call
- @current_tool_call =
- AnthropicToolCall.new(
- tool_name,
- tool_id,
- partial_tool_calls: @partial_tool_calls,
- ) if tool_name
- elsif parsed[:type] == "content_block_start" && parsed.dig(:content_block, :type) == "thinking"
- if @output_thinking
- @thinking =
- DiscourseAi::Completions::Thinking.new(
- message: +parsed.dig(:content_block, :thinking).to_s,
- signature: +"",
- partial: true,
- )
- result = @thinking.dup
- end
- elsif parsed[:type] == "content_block_delta" && parsed.dig(:delta, :type) == "thinking_delta"
- if @output_thinking
- delta = parsed.dig(:delta, :thinking)
- @thinking.message << delta if @thinking
- result = DiscourseAi::Completions::Thinking.new(message: delta, partial: true)
- end
- elsif parsed[:type] == "content_block_delta" && parsed.dig(:delta, :type) == "signature_delta"
- if @output_thinking
- @thinking.signature << parsed.dig(:delta, :signature) if @thinking
- end
- elsif parsed[:type] == "content_block_stop" && @thinking
- @thinking.partial = false
- result = @thinking
- @thinking = nil
- elsif parsed[:type] == "content_block_start" || parsed[:type] == "content_block_delta"
- if @current_tool_call
- tool_delta = parsed.dig(:delta, :partial_json).to_s
- @current_tool_call.append(tool_delta)
- result = @current_tool_call.partial_tool_call if @current_tool_call.has_partial?
- elsif parsed.dig(:content_block, :type) == "redacted_thinking"
- if @output_thinking
- result =
- DiscourseAi::Completions::Thinking.new(
- message: nil,
- signature: parsed.dig(:content_block, :data),
- redacted: true,
- )
- end
- else
- result = parsed.dig(:delta, :text).to_s
- # no need to return empty strings for streaming, no value
- result = nil if result == ""
- end
- elsif parsed[:type] == "content_block_stop"
- if @current_tool_call
- result = @current_tool_call.to_tool_call
- @current_tool_call = nil
- end
- elsif parsed[:type] == "message_start"
- @input_tokens = parsed.dig(:message, :usage, :input_tokens)
- elsif parsed[:type] == "message_delta"
- @output_tokens =
- parsed.dig(:usage, :output_tokens) || parsed.dig(:delta, :usage, :output_tokens)
- elsif parsed[:type] == "message_stop"
- # bedrock has this ...
- if bedrock_stats = parsed.dig("amazon-bedrock-invocationMetrics".to_sym)
- @input_tokens = bedrock_stats[:inputTokenCount] || @input_tokens
- @output_tokens = bedrock_stats[:outputTokenCount] || @output_tokens
- end
- end
- result
- end
-
- def process_message(payload)
- result = ""
- parsed = payload
- parsed = JSON.parse(payload, symbolize_names: true) if payload.is_a?(String)
-
- content = parsed.dig(:content)
- if content.is_a?(Array)
- result =
- content
- .map do |data|
- if data[:type] == "tool_use"
- call = AnthropicToolCall.new(data[:name], data[:id])
- call.append(data[:input].to_json)
- call.to_tool_call
- elsif data[:type] == "thinking"
- if @output_thinking
- DiscourseAi::Completions::Thinking.new(
- message: data[:thinking],
- signature: data[:signature],
- )
- end
- elsif data[:type] == "redacted_thinking"
- if @output_thinking
- DiscourseAi::Completions::Thinking.new(
- message: nil,
- signature: data[:data],
- redacted: true,
- )
- end
- else
- data[:text]
- end
- end
- .compact
- end
-
- @input_tokens = parsed.dig(:usage, :input_tokens)
- @output_tokens = parsed.dig(:usage, :output_tokens)
-
- result
- end
-end
diff --git a/lib/completions/cancel_manager.rb b/lib/completions/cancel_manager.rb
deleted file mode 100644
index 78c3ee5b..00000000
--- a/lib/completions/cancel_manager.rb
+++ /dev/null
@@ -1,109 +0,0 @@
-# frozen_string_literal: true
-
-# special object that can be used to cancel completions and http requests
-module DiscourseAi
- module Completions
- class CancelManager
- attr_reader :cancelled
- attr_reader :callbacks
-
- def initialize
- @cancelled = false
- @callbacks = Concurrent::Array.new
- @mutex = Mutex.new
- @monitor_thread = nil
- end
-
- def monitor_thread
- @mutex.synchronize { @monitor_thread }
- end
-
- def start_monitor(delay: 0.5, &block)
- @mutex.synchronize do
- raise "Already monitoring" if @monitor_thread
- raise "Expected a block" if !block
-
- db = RailsMultisite::ConnectionManagement.current_db
- @stop_monitor = false
-
- @monitor_thread =
- Thread.new do
- begin
- loop do
- done = false
- @mutex.synchronize { done = true if @stop_monitor }
- break if done
- sleep delay
- @mutex.synchronize { done = true if @stop_monitor }
- @mutex.synchronize { done = true if cancelled? }
- break if done
-
- should_cancel = false
- RailsMultisite::ConnectionManagement.with_connection(db) do
- should_cancel = block.call
- end
-
- @mutex.synchronize { cancel! if should_cancel }
-
- break if cancelled?
- end
- ensure
- @mutex.synchronize { @monitor_thread = nil }
- end
- end
- end
- end
-
- def stop_monitor
- monitor_thread = nil
-
- @mutex.synchronize { monitor_thread = @monitor_thread }
-
- if monitor_thread
- @mutex.synchronize { @stop_monitor = true }
- # so we do not deadlock
- monitor_thread.wakeup
- monitor_thread.join(2)
- # should not happen
- if monitor_thread.alive?
- Rails.logger.warn("DiscourseAI: CancelManager monitor thread did not stop in time")
- monitor_thread.kill if monitor_thread.alive?
- end
- @monitor_thread = nil
- end
- end
-
- def cancelled?
- @cancelled
- end
-
- def add_callback(cb)
- @callbacks << cb
- end
-
- def remove_callback(cb)
- @callbacks.delete(cb)
- end
-
- def cancel!
- @cancelled = true
- monitor_thread = @monitor_thread
- if monitor_thread && monitor_thread != Thread.current
- monitor_thread.wakeup
- monitor_thread.join(2)
- if monitor_thread.alive?
- Rails.logger.warn("DiscourseAI: CancelManager monitor thread did not stop in time")
- monitor_thread.kill if monitor_thread.alive?
- end
- end
- @callbacks.each do |cb|
- begin
- cb.call
- rescue StandardError
- # ignore cause this may have already been cancelled
- end
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/chat_gpt.rb b/lib/completions/dialects/chat_gpt.rb
deleted file mode 100644
index 3e29dcdd..00000000
--- a/lib/completions/dialects/chat_gpt.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class ChatGpt < Dialect
- class << self
- def can_translate?(llm_model)
- llm_model.provider == "open_router" || llm_model.provider == "open_ai" ||
- llm_model.provider == "azure"
- end
- end
-
- VALID_ID_REGEX = /\A[a-zA-Z0-9_]+\z/
-
- def native_tool_support?
- llm_model.provider == "open_ai" || llm_model.provider == "azure"
- end
-
- def embed_user_ids?
- return @embed_user_ids if defined?(@embed_user_ids)
-
- @embed_user_ids = true if responses_api?
-
- @embed_user_ids ||=
- prompt.messages.any? do |m|
- m[:id] && m[:type] == :user && !m[:id].to_s.match?(VALID_ID_REGEX)
- end
- end
-
- def responses_api?
- return @responses_api if defined?(@responses_api)
- @responses_api = llm_model.lookup_custom_param("enable_responses_api")
- end
-
- def max_prompt_tokens
- # provide a buffer of 120 tokens - our function counting is not
- # 100% accurate and getting numbers to align exactly is very hard
- buffer = (opts[:max_tokens] || 2500) + 50
-
- if tools.present?
- # note this is about 100 tokens over, OpenAI have a more optimal representation
- @function_size ||= llm_model.tokenizer_class.size(tools.to_json.to_s)
- buffer += @function_size
- end
-
- llm_model.max_prompt_tokens - buffer
- end
-
- def disable_native_tools?
- return @disable_native_tools if defined?(@disable_native_tools)
- !!@disable_native_tools = llm_model.lookup_custom_param("disable_native_tools")
- end
-
- private
-
- def tools_dialect
- if disable_native_tools?
- super
- else
- @tools_dialect ||=
- DiscourseAi::Completions::Dialects::OpenAiTools.new(
- prompt.tools,
- responses_api: responses_api?,
- )
- end
- end
-
- # developer messages are preferred on recent reasoning models
- def supports_developer_messages?
- !legacy_reasoning_model? && llm_model.provider == "open_ai" &&
- (llm_model.name.start_with?("o1") || llm_model.name.start_with?("o3"))
- end
-
- def legacy_reasoning_model?
- llm_model.provider == "open_ai" &&
- (llm_model.name.start_with?("o1-preview") || llm_model.name.start_with?("o1-mini"))
- end
-
- def system_msg(msg)
- content = msg[:content]
- if disable_native_tools? && tools_dialect.instructions.present?
- content = content + "\n\n" + tools_dialect.instructions
- end
-
- if supports_developer_messages?
- { role: "developer", content: content }
- elsif legacy_reasoning_model?
- { role: "user", content: content }
- else
- { role: "system", content: content }
- end
- end
-
- def model_msg(msg)
- { role: "assistant", content: msg[:content] }
- end
-
- def tool_call_msg(msg)
- if disable_native_tools?
- super
- else
- tools_dialect.from_raw_tool_call(msg)
- end
- end
-
- def tool_msg(msg)
- if disable_native_tools?
- super
- else
- tools_dialect.from_raw_tool(msg)
- end
- end
-
- def user_msg(msg)
- content_array = []
-
- user_message = { role: "user" }
-
- if msg[:id]
- if embed_user_ids?
- content_array << "#{msg[:id]}: "
- else
- user_message[:name] = msg[:id]
- end
- end
-
- content_array << msg[:content]
-
- content_array =
- to_encoded_content_array(
- content: content_array.flatten,
- image_encoder: ->(details) { image_node(details) },
- text_encoder: ->(text) { text_node(text) },
- allow_vision: vision_support?,
- )
-
- user_message[:content] = no_array_if_only_text(content_array)
- user_message
- end
-
- def no_array_if_only_text(content_array)
- if content_array.size == 1 && content_array.first[:type] == "text"
- content_array.first[:text]
- else
- content_array
- end
- end
-
- def text_node(text)
- if responses_api?
- { type: "input_text", text: text }
- else
- { type: "text", text: text }
- end
- end
-
- def image_node(details)
- encoded_image = "data:#{details[:mime_type]};base64,#{details[:base64]}"
- if responses_api?
- { type: "input_image", image_url: encoded_image }
- else
- { type: "image_url", image_url: { url: encoded_image } }
- end
- end
-
- def per_message_overhead
- # open ai defines about 4 tokens per message of overhead
- 4
- end
-
- def calculate_message_token(context)
- llm_model.tokenizer_class.size(context[:content].to_s + context[:name].to_s)
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/claude.rb b/lib/completions/dialects/claude.rb
deleted file mode 100644
index 0ad75624..00000000
--- a/lib/completions/dialects/claude.rb
+++ /dev/null
@@ -1,169 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class Claude < Dialect
- class << self
- def can_translate?(llm_model)
- llm_model.provider == "anthropic" ||
- (llm_model.provider == "aws_bedrock") &&
- (llm_model.name.include?("anthropic") || llm_model.name.include?("claude"))
- end
- end
-
- class ClaudePrompt
- attr_reader :system_prompt, :messages, :tools, :tool_choice
-
- def initialize(system_prompt, messages, tools, tool_choice)
- @system_prompt = system_prompt
- @messages = messages
- @tools = tools
- @tool_choice = tool_choice
- end
-
- def has_tools?
- tools.present?
- end
- end
-
- def translate
- messages = super
-
- system_prompt = messages.shift[:content] if messages.first[:role] == "system"
-
- if !system_prompt && !native_tool_support?
- system_prompt = tools_dialect.instructions.presence
- end
-
- interleving_messages = []
- previous_message = nil
-
- messages.each do |message|
- if previous_message
- if previous_message[:role] == "user" && message[:role] == "user"
- interleving_messages << { role: "assistant", content: "OK" }
- elsif previous_message[:role] == "assistant" && message[:role] == "assistant"
- interleving_messages << { role: "user", content: "OK" }
- end
- end
- interleving_messages << message
- previous_message = message
- end
-
- tools = nil
- tools = tools_dialect.translated_tools if native_tool_support?
-
- ClaudePrompt.new(system_prompt.presence, interleving_messages, tools, tool_choice)
- end
-
- def max_prompt_tokens
- llm_model.max_prompt_tokens
- end
-
- def native_tool_support?
- !llm_model.lookup_custom_param("disable_native_tools")
- end
-
- private
-
- def tools_dialect
- if native_tool_support?
- @tools_dialect ||= DiscourseAi::Completions::Dialects::ClaudeTools.new(prompt.tools)
- else
- super
- end
- end
-
- def tool_call_msg(msg)
- translated = tools_dialect.from_raw_tool_call(msg)
- { role: "assistant", content: translated }
- end
-
- def tool_msg(msg)
- translated = tools_dialect.from_raw_tool(msg)
- { role: "user", content: translated }
- end
-
- def model_msg(msg)
- content_array = []
-
- if msg[:thinking] || msg[:redacted_thinking_signature]
- if msg[:thinking]
- content_array << {
- type: "thinking",
- thinking: msg[:thinking],
- signature: msg[:thinking_signature],
- }
- end
-
- if msg[:redacted_thinking_signature]
- content_array << {
- type: "redacted_thinking",
- data: msg[:redacted_thinking_signature],
- }
- end
- end
-
- # other encoder is used to pass through thinking
- content_array =
- to_encoded_content_array(
- content: [content_array, msg[:content]].flatten,
- image_encoder: ->(details) {},
- text_encoder: ->(text) { { type: "text", text: text } },
- other_encoder: ->(details) { details },
- allow_vision: false,
- )
-
- { role: "assistant", content: no_array_if_only_text(content_array) }
- end
-
- def system_msg(msg)
- msg = { role: "system", content: msg[:content] }
-
- if tools_dialect.instructions.present?
- msg[:content] = msg[:content].dup << "\n\n#{tools_dialect.instructions}"
- end
-
- msg
- end
-
- def user_msg(msg)
- content_array = []
- content_array << "#{msg[:id]}: " if msg[:id]
- content_array.concat([msg[:content]].flatten)
-
- content_array =
- to_encoded_content_array(
- content: content_array,
- image_encoder: ->(details) { image_node(details) },
- text_encoder: ->(text) { { type: "text", text: text } },
- allow_vision: vision_support?,
- )
-
- { role: "user", content: no_array_if_only_text(content_array) }
- end
-
- # keeping our payload as backward compatible as possible
- def no_array_if_only_text(content_array)
- if content_array.length == 1 && content_array.first[:type] == "text"
- content_array.first[:text]
- else
- content_array
- end
- end
-
- def image_node(details)
- {
- source: {
- type: "base64",
- data: details[:base64],
- media_type: details[:mime_type],
- },
- type: "image",
- }
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/claude_tools.rb b/lib/completions/dialects/claude_tools.rb
deleted file mode 100644
index 48ef0232..00000000
--- a/lib/completions/dialects/claude_tools.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class ClaudeTools
- def initialize(tools)
- @raw_tools = tools
- end
-
- def translated_tools
- raw_tools.map do |t|
- { name: t.name, description: t.description, input_schema: t.parameters_json_schema }
- end
- end
-
- def instructions
- ""
- end
-
- def from_raw_tool_call(raw_message)
- call_details = JSON.parse(raw_message[:content], symbolize_names: true)
- result = []
-
- if raw_message[:thinking] || raw_message[:redacted_thinking_signature]
- if raw_message[:thinking]
- result << {
- type: "thinking",
- thinking: raw_message[:thinking],
- signature: raw_message[:thinking_signature],
- }
- end
-
- if raw_message[:redacted_thinking_signature]
- result << {
- type: "redacted_thinking",
- data: raw_message[:redacted_thinking_signature],
- }
- end
- end
-
- tool_call_id = raw_message[:id]
-
- result << {
- type: "tool_use",
- id: tool_call_id,
- name: raw_message[:name],
- input: call_details[:arguments],
- }
-
- result
- end
-
- def from_raw_tool(raw_message)
- [{ type: "tool_result", tool_use_id: raw_message[:id], content: raw_message[:content] }]
- end
-
- private
-
- attr_reader :raw_tools
- end
- end
- end
-end
diff --git a/lib/completions/dialects/cohere_tools.rb b/lib/completions/dialects/cohere_tools.rb
deleted file mode 100644
index 26eb3bc7..00000000
--- a/lib/completions/dialects/cohere_tools.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class CohereTools
- def initialize(tools)
- @raw_tools = tools
- end
-
- def tool_results(messages)
- pairs = []
-
- current_pair = nil
- messages.each do |msg|
- if current_pair == nil && msg[:type] == :tool_call
- current_pair = [msg]
- elsif current_pair && msg[:type] == :tool
- current_pair << msg
- pairs << current_pair
- current_pair = nil
- else
- current_pair = nil
- end
- end
-
- pairs.map do |call, result|
- params = JSON.parse(call[:content])["arguments"]
- {
- call: {
- name: call[:name] == "search" ? "search_local" : call[:name],
- parameters: params,
- generation_id: call[:id],
- },
- outputs: [JSON.parse(result[:content])],
- }
- end
- end
-
- def translated_tools
- raw_tools.map do |tool|
- defs = {}
-
- tool.parameters.each do |p|
- name = p.name
- defs[name] = {
- description: p.description,
- type: cohere_type(p.type, p.item_type),
- required: p.required,
- }
-
- #defs[name][:default] = p.default if p.default
- end
-
- {
- name: tool.name == "search" ? "search_local" : tool.name,
- description: tool.description,
- parameter_definitions: defs,
- }
- end
- end
-
- def instructions
- "" # Noop. Tools are listed separate.
- end
-
- private
-
- attr_reader :raw_tools
-
- def cohere_type(type, item_type)
- type = type.to_s
- case type
- when "string"
- "str"
- when "number"
- item_type == "integer" ? "int" : "float"
- when "boolean"
- "bool"
- when "object"
- item_type ? "Dict[#{item_type}]" : "Dict"
- when "array"
- item_type ? "List[#{item_type}]" : "List"
- else
- type
- end
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/command.rb b/lib/completions/dialects/command.rb
deleted file mode 100644
index ce390d8e..00000000
--- a/lib/completions/dialects/command.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-# frozen_string_literal: true
-
-# see: https://docs.cohere.com/reference/chat
-#
-module DiscourseAi
- module Completions
- module Dialects
- class Command < Dialect
- def self.can_translate?(llm_model)
- llm_model.provider == "cohere"
- end
-
- VALID_ID_REGEX = /\A[a-zA-Z0-9_]+\z/
-
- def translate
- messages = super
-
- system_message = messages.shift[:message] if messages.first[:role] == "SYSTEM"
-
- prompt = { preamble: +"#{system_message}" }
-
- if messages.present?
- with_mapped_tools = []
-
- current_pair = nil
- messages.each do |msg|
- if current_pair == nil && msg[:type] == :tool_call
- current_pair = [msg]
- elsif current_pair && msg[:type] == :tool
- current_pair << msg
- tool_results = tools_dialect.tool_results(current_pair)
- with_mapped_tools << { role: "TOOL", message: "", tool_results: tool_results }
- current_pair = nil
- else
- with_mapped_tools << msg
- current_pair = nil
- end
- end
-
- messages = with_mapped_tools
- prompt[:chat_history] = messages
- end
-
- tools = tools_dialect.translated_tools
- prompt[:tools] = tools if tools.present?
-
- tool_results =
- messages.last && messages.last[:role] == "TOOL" && messages.last[:tool_results]
- prompt[:tool_results] = tool_results if tool_results.present?
-
- if tool_results.blank?
- messages.reverse_each do |msg|
- if msg[:role] == "USER"
- prompt[:message] = msg[:message]
- messages.delete(msg)
- break
- end
- end
- end
-
- prompt
- end
-
- def max_prompt_tokens
- llm_model.max_prompt_tokens
- end
-
- def native_tool_support?
- true
- end
-
- private
-
- def tools_dialect
- @tools_dialect ||= DiscourseAi::Completions::Dialects::CohereTools.new(prompt.tools)
- end
-
- def per_message_overhead
- 0
- end
-
- def calculate_message_token(context)
- llm_model.tokenizer_class.size(context[:content].to_s + context[:name].to_s)
- end
-
- def system_msg(msg)
- cmd_msg = { role: "SYSTEM", message: msg[:content] }
-
- if tools_dialect.instructions.present?
- cmd_msg[:message] = [
- msg[:content],
- tools_dialect.instructions,
- "NEVER attempt to run tools using JSON, always use XML. Lives depend on it.",
- ].join("\n")
- end
-
- cmd_msg
- end
-
- def model_msg(msg)
- { role: "CHATBOT", message: msg[:content] }
- end
-
- def tool_call_msg(msg)
- msg
- end
-
- def tool_msg(msg)
- msg
- end
-
- def user_msg(msg)
- content = DiscourseAi::Completions::Prompt.text_only(msg)
- user_message = { role: "USER", message: content }
- user_message[:message] = "#{msg[:id]}: #{content}" if msg[:id]
- user_message
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/dialect.rb b/lib/completions/dialects/dialect.rb
deleted file mode 100644
index 82f2aff3..00000000
--- a/lib/completions/dialects/dialect.rb
+++ /dev/null
@@ -1,266 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class Dialect
- class << self
- def can_translate?(llm_model)
- raise NotImplemented
- end
-
- def all_dialects
- [
- DiscourseAi::Completions::Dialects::ChatGpt,
- DiscourseAi::Completions::Dialects::Gemini,
- DiscourseAi::Completions::Dialects::Claude,
- DiscourseAi::Completions::Dialects::Command,
- DiscourseAi::Completions::Dialects::Ollama,
- DiscourseAi::Completions::Dialects::Mistral,
- DiscourseAi::Completions::Dialects::Nova,
- DiscourseAi::Completions::Dialects::OpenAiCompatible,
- ]
- end
-
- def dialect_for(llm_model)
- dialects = []
-
- if Rails.env.test? || Rails.env.development?
- dialects = [DiscourseAi::Completions::Dialects::Fake]
- end
-
- dialects = dialects.concat(all_dialects)
-
- dialect = dialects.find { |d| d.can_translate?(llm_model) }
- raise DiscourseAi::Completions::Llm::UNKNOWN_MODEL if !dialect
-
- dialect
- end
- end
-
- def initialize(generic_prompt, llm_model, opts: {})
- @prompt = generic_prompt
- @opts = opts
- @llm_model = llm_model
- end
-
- VALID_ID_REGEX = /\A[a-zA-Z0-9_]+\z/
-
- def native_tool_support?
- false
- end
-
- def vision_support?
- llm_model.vision_enabled?
- end
-
- def tools
- @tools ||= tools_dialect.translated_tools
- end
-
- def tool_choice
- prompt.tool_choice
- end
-
- def self.no_more_tool_calls_text
- # note, Anthropic must never prefill with an ending whitespace
- "I WILL NOT USE TOOLS IN THIS REPLY, user expressed they wanted to stop using tool calls.\nHere is the best, complete, answer I can come up with given the information I have."
- end
-
- def self.no_more_tool_calls_text_user
- "DO NOT USE TOOLS IN YOUR REPLY. Return the best answer you can given the information I supplied you."
- end
-
- def no_more_tool_calls_text
- self.class.no_more_tool_calls_text
- end
-
- def no_more_tool_calls_text_user
- self.class.no_more_tool_calls_text_user
- end
-
- def translate
- messages = trim_messages(prompt.messages)
- last_message = messages.last
- inject_done_on_last_tool_call = false
-
- if !native_tool_support? && last_message && last_message[:type].to_sym == :tool &&
- prompt.tool_choice == :none
- inject_done_on_last_tool_call = true
- end
-
- translated =
- messages
- .map do |msg|
- case msg[:type].to_sym
- when :system
- system_msg(msg)
- when :user
- user_msg(msg)
- when :model
- model_msg(msg)
- when :tool
- if inject_done_on_last_tool_call && msg == last_message
- tools_dialect.inject_done { tool_msg(msg) }
- else
- tool_msg(msg)
- end
- when :tool_call
- tool_call_msg(msg)
- else
- raise ArgumentError, "Unknown message type: #{msg[:type]}"
- end
- end
- .compact
-
- translated
- end
-
- def conversation_context
- raise NotImplemented
- end
-
- def max_prompt_tokens
- raise NotImplemented
- end
-
- attr_reader :prompt
-
- private
-
- attr_reader :opts, :llm_model
-
- def trim_messages(messages)
- prompt_limit = max_prompt_tokens
- current_token_count = 0
- message_step_size = (prompt_limit / 25).to_i * -1
-
- trimmed_messages = []
-
- range = (0..-1)
- if messages.dig(0, :type) == :system
- max_system_tokens = prompt_limit * 0.6
- system_message = messages[0]
- system_size = calculate_message_token(system_message)
-
- if system_size > max_system_tokens
- system_message[:content] = tokenizer.truncate(
- system_message[:content],
- max_system_tokens,
- strict: SiteSetting.ai_strict_token_counting,
- )
- end
-
- trimmed_messages << system_message
- current_token_count += calculate_message_token(system_message)
- range = (1..-1)
- end
-
- reversed_trimmed_msgs = []
-
- messages[range].reverse.each do |msg|
- break if current_token_count >= prompt_limit
-
- message_tokens = calculate_message_token(msg)
-
- dupped_msg = msg.dup
-
- # Don't trim tool call metadata.
- if msg[:type] == :tool_call
- break if current_token_count + message_tokens + per_message_overhead > prompt_limit
-
- current_token_count += message_tokens + per_message_overhead
- reversed_trimmed_msgs << dupped_msg
- next
- end
-
- # Trimming content to make sure we respect token limit.
- while dupped_msg[:content].present? &&
- message_tokens + current_token_count + per_message_overhead > prompt_limit
- dupped_msg[:content] = dupped_msg[:content][0..message_step_size] || ""
- message_tokens = calculate_message_token(dupped_msg)
- end
-
- next if dupped_msg[:content].blank?
-
- current_token_count += message_tokens + per_message_overhead
-
- reversed_trimmed_msgs << dupped_msg
- end
-
- reversed_trimmed_msgs.pop if reversed_trimmed_msgs.last&.dig(:type) == :tool
-
- trimmed_messages.concat(reversed_trimmed_msgs.reverse)
- end
-
- def per_message_overhead
- 0
- end
-
- def calculate_message_token(msg)
- llm_model.tokenizer_class.size(msg[:content].to_s)
- end
-
- def tools_dialect
- @tools_dialect ||= DiscourseAi::Completions::Dialects::XmlTools.new(prompt.tools)
- end
-
- def system_msg(msg)
- raise NotImplemented
- end
-
- def model_msg(msg)
- raise NotImplemented
- end
-
- def user_msg(msg)
- raise NotImplemented
- end
-
- def tool_call_msg(msg)
- new_content = tools_dialect.from_raw_tool_call(msg)
- msg = msg.merge(content: new_content)
- model_msg(msg)
- end
-
- def tool_msg(msg)
- new_content = tools_dialect.from_raw_tool(msg)
- msg = msg.merge(content: new_content)
- user_msg(msg)
- end
-
- def to_encoded_content_array(
- content:,
- image_encoder:,
- text_encoder:,
- other_encoder: nil,
- allow_vision:
- )
- content = [content] if !content.is_a?(Array)
-
- current_string = +""
- result = []
-
- content.each do |c|
- if c.is_a?(String)
- current_string << c
- elsif c.is_a?(Hash) && c.key?(:upload_id) && allow_vision
- if !current_string.empty?
- result << text_encoder.call(current_string)
- current_string = +""
- end
- encoded = prompt.encode_upload(c[:upload_id])
- result << image_encoder.call(encoded) if encoded
- elsif other_encoder
- encoded = other_encoder.call(c)
- result << encoded if encoded
- end
- end
-
- result << text_encoder.call(current_string) if !current_string.empty?
- result
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/fake.rb b/lib/completions/dialects/fake.rb
deleted file mode 100644
index cda44110..00000000
--- a/lib/completions/dialects/fake.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class Fake < Dialect
- class << self
- def can_translate?(llm_model)
- llm_model.provider == "fake"
- end
- end
-
- def tokenizer
- DiscourseAi::Tokenizer::OpenAiTokenizer
- end
-
- def translate
- ""
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/gemini.rb b/lib/completions/dialects/gemini.rb
deleted file mode 100644
index 8ffbf5d7..00000000
--- a/lib/completions/dialects/gemini.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class Gemini < Dialect
- class << self
- def can_translate?(llm_model)
- llm_model.provider == "google"
- end
- end
-
- def native_tool_support?
- !llm_model.lookup_custom_param("disable_native_tools")
- end
-
- def translate
- # Gemini complains if we don't alternate model/user roles.
- noop_model_response = { role: "model", parts: { text: "Ok." } }
- messages = super
-
- interleving_messages = []
- previous_message = nil
-
- system_instruction = nil
-
- messages.each do |message|
- if message[:role] == "system"
- system_instruction = message[:content]
- next
- end
- if previous_message
- if (previous_message[:role] == "user" || previous_message[:role] == "function") &&
- message[:role] == "user"
- interleving_messages << noop_model_response.dup
- end
- end
- interleving_messages << message
- previous_message = message
- end
-
- { messages: interleving_messages, system_instruction: system_instruction }
- end
-
- def tools
- return if prompt.tools.blank?
-
- translated_tools =
- prompt.tools.map do |t|
- tool = { name: t.name, description: t.description }
- tool[:parameters] = t.parameters_json_schema if t.parameters
- tool
- end
-
- [{ function_declarations: translated_tools }]
- end
-
- def max_prompt_tokens
- llm_model.max_prompt_tokens
- end
-
- protected
-
- def calculate_message_token(context)
- llm_model.tokenizer_class.size(context[:content].to_s + context[:name].to_s)
- end
-
- def beta_api?
- @beta_api ||= !llm_model.name.start_with?("gemini-1.0")
- end
-
- def system_msg(msg)
- content = msg[:content]
-
- if !native_tool_support? && tools_dialect.instructions.present?
- content = content.to_s + "\n\n#{tools_dialect.instructions}"
- end
-
- if beta_api?
- { role: "system", content: content }
- else
- { role: "user", parts: { text: content } }
- end
- end
-
- def model_msg(msg)
- if beta_api?
- { role: "model", parts: [{ text: msg[:content] }] }
- else
- { role: "model", parts: { text: msg[:content] } }
- end
- end
-
- def user_msg(msg)
- content_array = []
- content_array << "#{msg[:id]}: " if msg[:id]
-
- content_array << msg[:content]
- content_array.flatten!
-
- content_array =
- to_encoded_content_array(
- content: content_array,
- image_encoder: ->(details) { image_node(details) },
- text_encoder: ->(text) { { text: text } },
- allow_vision: vision_support? && beta_api?,
- )
-
- if beta_api?
- { role: "user", parts: content_array }
- else
- { role: "user", parts: content_array.first }
- end
- end
-
- def image_node(details)
- { inlineData: { mimeType: details[:mime_type], data: details[:base64] } }
- end
-
- def tool_call_msg(msg)
- if native_tool_support?
- call_details = JSON.parse(msg[:content], symbolize_names: true)
- part = {
- functionCall: {
- name: msg[:name] || call_details[:name],
- args: call_details[:arguments],
- },
- }
-
- if beta_api?
- { role: "model", parts: [part] }
- else
- { role: "model", parts: part }
- end
- else
- super
- end
- end
-
- def tool_msg(msg)
- if native_tool_support?
- part = {
- functionResponse: {
- name: msg[:name] || msg[:id],
- response: {
- content: msg[:content],
- },
- },
- }
-
- if beta_api?
- { role: "function", parts: [part] }
- else
- { role: "function", parts: part }
- end
- else
- super
- end
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/mistral.rb b/lib/completions/dialects/mistral.rb
deleted file mode 100644
index eaf5eab8..00000000
--- a/lib/completions/dialects/mistral.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-# basically the same as Open AI, except for no support for user names
-
-module DiscourseAi
- module Completions
- module Dialects
- class Mistral < ChatGpt
- class << self
- def can_translate?(llm_model)
- llm_model.provider == "mistral"
- end
- end
-
- def translate
- corrected = super
- corrected.each do |msg|
- msg[:content] = "" if msg[:tool_calls] && msg[:role] == "assistant"
- end
- corrected
- end
-
- private
-
- def user_msg(msg)
- mapped = super
- if name = mapped.delete(:name)
- if mapped[:content].is_a?(String)
- mapped[:content] = "#{name}: #{mapped[:content]}"
- else
- mapped[:content].each do |inner|
- if inner[:text]
- inner[:text] = "#{name}: #{inner[:text]}"
- break
- end
- end
- end
- end
- mapped
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/nova.rb b/lib/completions/dialects/nova.rb
deleted file mode 100644
index 10098c26..00000000
--- a/lib/completions/dialects/nova.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class Nova < Dialect
- class << self
- def can_translate?(llm_model)
- llm_model.provider == "aws_bedrock" && llm_model.name.include?("amazon.nova")
- end
- end
-
- class NovaPrompt
- attr_reader :system, :messages, :inference_config, :tool_config
-
- def initialize(system, messages, inference_config = nil, tool_config = nil)
- @system = system
- @messages = messages
- @inference_config = inference_config
- @tool_config = tool_config
- end
-
- def system_prompt
- # small hack for size estimation
- system.to_s
- end
-
- def has_tools?
- tool_config.present?
- end
-
- def to_payload(options = nil)
- stop_sequences = options[:stop_sequences]
- max_tokens = options[:max_tokens]
-
- inference_config = options&.slice(:temperature, :top_p, :top_k)
-
- inference_config[:stopSequences] = stop_sequences if stop_sequences.present?
-
- inference_config[:max_new_tokens] = max_tokens if max_tokens.present?
-
- result = { system: system, messages: messages }
- result[:inferenceConfig] = inference_config if inference_config.present?
- result[:toolConfig] = tool_config if tool_config.present?
- result[:response_format] = { type: "json_object" } if options[:response_format].present?
-
- result
- end
- end
-
- def translate
- messages = super
-
- system = messages.shift[:content] if messages.first&.dig(:role) == "system"
- nova_messages = messages.map { |msg| { role: msg[:role], content: build_content(msg) } }
-
- inference_config = build_inference_config
- tool_config = tools_dialect.translated_tools if native_tool_support?
-
- NovaPrompt.new(
- system.presence && [{ text: system }],
- nova_messages,
- inference_config,
- tool_config,
- )
- end
-
- def max_prompt_tokens
- llm_model.max_prompt_tokens
- end
-
- def native_tool_support?
- !llm_model.lookup_custom_param("disable_native_tools")
- end
-
- def tools_dialect
- if native_tool_support?
- @tools_dialect ||= DiscourseAi::Completions::Dialects::NovaTools.new(prompt.tools)
- else
- super
- end
- end
-
- private
-
- def build_content(msg)
- content = []
-
- existing_content = msg[:content]
-
- if existing_content.is_a?(Hash)
- content << existing_content
- elsif existing_content.is_a?(String)
- content << { text: existing_content }
- end
-
- msg[:images]&.each { |image| content << image }
-
- content
- end
-
- def build_inference_config
- return unless opts[:inference_config]
-
- config = {}
- ic = opts[:inference_config]
-
- config[:max_new_tokens] = ic[:max_new_tokens] if ic[:max_new_tokens]
- config[:temperature] = ic[:temperature] if ic[:temperature]
- config[:top_p] = ic[:top_p] if ic[:top_p]
- config[:top_k] = ic[:top_k] if ic[:top_k]
- config[:stopSequences] = ic[:stop_sequences] if ic[:stop_sequences]
-
- config.present? ? config : nil
- end
-
- def detect_format(mime_type)
- case mime_type
- when "image/jpeg"
- "jpeg"
- when "image/png"
- "png"
- when "image/gif"
- "gif"
- when "image/webp"
- "webp"
- else
- "jpeg" # default
- end
- end
-
- def system_msg(msg)
- msg = { role: "system", content: msg[:content] }
-
- if tools_dialect.instructions.present?
- msg[:content] = msg[:content].dup << "\n\n#{tools_dialect.instructions}"
- end
-
- msg
- end
-
- def user_msg(msg)
- images = nil
- if vision_support?
- encoded_uploads = prompt.encoded_uploads(msg)
- encoded_uploads&.each do |upload|
- images ||= []
- images << {
- image: {
- format: upload[:format] || detect_format(upload[:mime_type]),
- source: {
- bytes: upload[:base64],
- },
- },
- }
- end
- end
-
- { role: "user", content: DiscourseAi::Completions::Prompt.text_only(msg), images: images }
- end
-
- def model_msg(msg)
- { role: "assistant", content: msg[:content] }
- end
-
- def tool_msg(msg)
- translated = tools_dialect.from_raw_tool(msg)
- { role: "user", content: translated }
- end
-
- def tool_call_msg(msg)
- translated = tools_dialect.from_raw_tool_call(msg)
- { role: "assistant", content: translated }
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/nova_tools.rb b/lib/completions/dialects/nova_tools.rb
deleted file mode 100644
index 54d009a6..00000000
--- a/lib/completions/dialects/nova_tools.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class NovaTools
- def initialize(tools)
- @raw_tools = tools
- end
-
- def translated_tools
- return if !@raw_tools.present?
-
- # note: forced tools are not supported yet toolChoice is always auto
- {
- tools:
- @raw_tools.map do |tool|
- {
- toolSpec: {
- name: tool.name,
- description: tool.description,
- inputSchema: {
- json: tool.parameters_json_schema,
- },
- },
- }
- end,
- }
- end
-
- # nativ tools require no system instructions
- def instructions
- ""
- end
-
- def from_raw_tool_call(raw_message)
- {
- toolUse: {
- toolUseId: raw_message[:id],
- name: raw_message[:name],
- input: JSON.parse(raw_message[:content])["arguments"],
- },
- }
- end
-
- def from_raw_tool(raw_message)
- {
- toolResult: {
- toolUseId: raw_message[:id],
- content: [{ json: JSON.parse(raw_message[:content]) }],
- },
- }
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/ollama.rb b/lib/completions/dialects/ollama.rb
deleted file mode 100644
index 60d58455..00000000
--- a/lib/completions/dialects/ollama.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class Ollama < Dialect
- class << self
- def can_translate?(llm_model)
- llm_model.provider == "ollama"
- end
- end
-
- def native_tool_support?
- enable_native_tool?
- end
-
- def max_prompt_tokens
- llm_model.max_prompt_tokens
- end
-
- private
-
- def tools_dialect
- if enable_native_tool?
- @tools_dialect ||= DiscourseAi::Completions::Dialects::OllamaTools.new(prompt.tools)
- else
- super
- end
- end
-
- def tokenizer
- llm_model.tokenizer_class
- end
-
- def model_msg(msg)
- { role: "assistant", content: msg[:content] }
- end
-
- def tool_call_msg(msg)
- if enable_native_tool?
- tools_dialect.from_raw_tool_call(msg)
- else
- super
- end
- end
-
- def tool_msg(msg)
- if enable_native_tool?
- tools_dialect.from_raw_tool(msg)
- else
- super
- end
- end
-
- def system_msg(msg)
- msg = { role: "system", content: msg[:content] }
-
- if tools_dialect.instructions.present?
- msg[:content] = msg[:content].dup << "\n\n#{tools_dialect.instructions}"
- end
-
- msg
- end
-
- def enable_native_tool?
- return @enable_native_tool if defined?(@enable_native_tool)
-
- @enable_native_tool = llm_model.lookup_custom_param("enable_native_tool")
- end
-
- def user_msg(msg)
- user_message = { role: "user", content: DiscourseAi::Completions::Prompt.text_only(msg) }
-
- encoded_uploads = prompt.encoded_uploads(msg)
- if encoded_uploads.present?
- images =
- encoded_uploads
- .map do |upload|
- if upload[:mime_type].start_with?("image/")
- upload[:base64]
- else
- nil
- end
- end
- .compact
-
- user_message[:images] = images if images.present?
- end
-
- # TODO: Add support for user messages with embedded user ids
-
- user_message
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/ollama_tools.rb b/lib/completions/dialects/ollama_tools.rb
deleted file mode 100644
index 90cdb136..00000000
--- a/lib/completions/dialects/ollama_tools.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- # TODO: Define the Tool class to be inherited by all tools.
- class OllamaTools
- def initialize(tools)
- @raw_tools = tools
- end
-
- def instructions
- "" # Noop. Tools are listed separate.
- end
-
- def translated_tools
- raw_tools.map do |tool|
- {
- type: "function",
- function: {
- name: tool.name,
- description: tool.description,
- parameters: tool.parameters_json_schema,
- },
- }
- end
- end
-
- def from_raw_tool_call(raw_message)
- call_details = JSON.parse(raw_message[:content], symbolize_names: true)
- call_details[:name] = raw_message[:name]
-
- {
- role: "assistant",
- content: nil,
- tool_calls: [{ type: "function", function: call_details }],
- }
- end
-
- def from_raw_tool(raw_message)
- { role: "tool", content: raw_message[:content], name: raw_message[:name] }
- end
-
- private
-
- attr_reader :raw_tools
- end
- end
- end
-end
diff --git a/lib/completions/dialects/open_ai_compatible.rb b/lib/completions/dialects/open_ai_compatible.rb
deleted file mode 100644
index d6519822..00000000
--- a/lib/completions/dialects/open_ai_compatible.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class OpenAiCompatible < ChatGpt
- class << self
- def can_translate?(_llm_model)
- # fallback dialect
- true
- end
- end
-
- def tokenizer
- llm_model&.tokenizer_class || DiscourseAi::Tokenizer::Llama3Tokenizer
- end
-
- def tools
- @tools ||= tools_dialect.translated_tools
- end
-
- def max_prompt_tokens
- return llm_model.max_prompt_tokens if llm_model&.max_prompt_tokens
-
- 32_000
- end
-
- def translate
- translated = super
-
- return translated unless llm_model.lookup_custom_param("disable_system_prompt")
-
- system_msg, user_msg = translated.shift(2)
-
- if user_msg[:content].is_a?(Array) # Has inline images.
- user_msg[:content].first[:text] = [
- system_msg[:content],
- user_msg[:content].first[:text],
- ].join("\n")
- else
- user_msg[:content] = [system_msg[:content], user_msg[:content]].join("\n")
- end
-
- translated.unshift(user_msg)
- end
- end
- end
- end
-end
diff --git a/lib/completions/dialects/open_ai_tools.rb b/lib/completions/dialects/open_ai_tools.rb
deleted file mode 100644
index 3411dbfe..00000000
--- a/lib/completions/dialects/open_ai_tools.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class OpenAiTools
- def initialize(tools, responses_api: false)
- @responses_api = responses_api
- @raw_tools = tools
- end
-
- def translated_tools
- if @responses_api
- raw_tools.map do |tool|
- {
- type: "function",
- name: tool.name,
- description: tool.description,
- parameters: tool.parameters_json_schema,
- }
- end
- else
- raw_tools.map do |tool|
- {
- type: "function",
- function: {
- name: tool.name,
- description: tool.description,
- parameters: tool.parameters_json_schema,
- },
- }
- end
- end
- end
-
- def instructions
- "" # Noop. Tools are listed separate.
- end
-
- def from_raw_tool_call(raw_message)
- call_details = JSON.parse(raw_message[:content], symbolize_names: true)
- call_details[:arguments] = call_details[:arguments].to_json
- call_details[:name] = raw_message[:name]
-
- if @responses_api
- {
- type: "function_call",
- call_id: raw_message[:id],
- name: call_details[:name],
- arguments: call_details[:arguments],
- }
- else
- {
- role: "assistant",
- content: nil,
- tool_calls: [{ type: "function", function: call_details, id: raw_message[:id] }],
- }
- end
- end
-
- def from_raw_tool(raw_message)
- if @responses_api
- {
- type: "function_call_output",
- call_id: raw_message[:id],
- output: raw_message[:content],
- }
- else
- {
- role: "tool",
- tool_call_id: raw_message[:id],
- content: raw_message[:content],
- name: raw_message[:name],
- }
- end
- end
-
- private
-
- attr_reader :raw_tools
- end
- end
- end
-end
diff --git a/lib/completions/dialects/xml_tools.rb b/lib/completions/dialects/xml_tools.rb
deleted file mode 100644
index 7af04df1..00000000
--- a/lib/completions/dialects/xml_tools.rb
+++ /dev/null
@@ -1,149 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Dialects
- class XmlTools
- def initialize(tools)
- @raw_tools = tools
- end
-
- def translated_tools
- result = +""
-
- raw_tools.each do |tool|
- parameters = +""
- if tool.parameters.present?
- tool.parameters.each do |parameter|
- parameters << <<~PARAMETER
-
- #{parameter.name}
- #{parameter.type}
- #{parameter.description}
- #{parameter.required}
- PARAMETER
- if parameter.item_type
- parameters << "#{parameter.item_type}\n"
- end
- parameters << "#{parameter.enum.join(",")}\n" if parameter.enum
- parameters << "\n"
- end
- end
-
- result << <<~TOOLS
-
- #{tool.name}
- #{tool.description}
-
- #{parameters}
-
- TOOLS
- end
- result
- end
-
- def instructions
- return "" if raw_tools.blank?
-
- @instructions ||=
- begin
- has_arrays = raw_tools.any? { |tool| tool.parameters&.any? { |p| p.type == "array" } }
-
- (<<~TEXT).strip
- #{tool_preamble(include_array_tip: has_arrays)}
-
- #{translated_tools}
- TEXT
- end
- end
-
- DONE_MESSAGE =
- "Regardless of what you think, REPLY IMMEDIATELY, WITHOUT MAKING ANY FURTHER TOOL CALLS, YOU ARE OUT OF TOOL CALL QUOTA!"
-
- def from_raw_tool(raw_message)
- result = (<<~TEXT).strip
-
-
- #{raw_message[:name] || raw_message[:id]}
-
- #{raw_message[:content]}
-
-
-
- TEXT
-
- if @injecting_done
- "#{result}\n\n#{DONE_MESSAGE}"
- else
- result
- end
- end
-
- def from_raw_tool_call(raw_message)
- parsed = JSON.parse(raw_message[:content], symbolize_names: true)
- parameters = +""
-
- if parsed[:arguments]
- parameters << "\n"
- parsed[:arguments].each { |k, v| parameters << "<#{k}>#{v}#{k}>\n" }
- parameters << "\n"
- end
-
- (<<~TEXT).strip
-
-
- #{raw_message[:name] || parsed[:name]}
- #{parameters}
-
- TEXT
- end
-
- def inject_done(&blk)
- @injecting_done = true
- blk.call
- ensure
- @injecting_done = false
- end
-
- private
-
- attr_reader :raw_tools
-
- def tool_preamble(include_array_tip: true)
- array_tip =
- if include_array_tip
- <<~TEXT
- If a parameter type is an array, return an array of values. For example:
- <$PARAMETER_NAME>["one","two","three"]$PARAMETER_NAME>
- TEXT
- else
- ""
- end
-
- <<~TEXT
- In this environment you have access to a set of tools you can use to answer the user's question.
- You may call them like this.
-
-
-
- $TOOL_NAME
-
- <$PARAMETER_NAME>$PARAMETER_VALUE$PARAMETER_NAME>
- ...
-
-
-
- #{array_tip}
- If you wish to call multiple function in one reply, wrap multiple
- block in a single block.
-
- - Always prefer to lead with tool calls, if you need to execute any.
- - Avoid all niceties prior to tool calls, Eg: "Let me look this up for you.." etc.
- - DO NOT encode HTML entities in tool calls. You may use for encoding if required.
- Here are the complete list of tools available:
- TEXT
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/anthropic.rb b/lib/completions/endpoints/anthropic.rb
deleted file mode 100644
index 0ca940c5..00000000
--- a/lib/completions/endpoints/anthropic.rb
+++ /dev/null
@@ -1,180 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class Anthropic < Base
- def self.can_contact?(model_provider)
- model_provider == "anthropic"
- end
-
- def normalize_model_params(model_params)
- # max_tokens, temperature, stop_sequences are already supported
- model_params = model_params.dup
- model_params.delete(:top_p) if llm_model.lookup_custom_param("disable_top_p")
- model_params.delete(:temperature) if llm_model.lookup_custom_param("disable_temperature")
- model_params
- end
-
- def default_options(dialect)
- mapped_model =
- case llm_model.name
- when "claude-2"
- "claude-2.1"
- when "claude-instant-1"
- "claude-instant-1.2"
- when "claude-3-haiku"
- "claude-3-haiku-20240307"
- when "claude-3-sonnet"
- "claude-3-sonnet-20240229"
- when "claude-3-opus"
- "claude-3-opus-20240229"
- when "claude-3-5-sonnet"
- "claude-3-5-sonnet-latest"
- when "claude-3-7-sonnet"
- "claude-3-7-sonnet-latest"
- when "claude-4-opus"
- "claude-4-opus-20250514"
- when "claude-4-sonnet"
- "claude-4-sonnet-20250514"
- else
- llm_model.name
- end
-
- # Note: Anthropic requires this param
- max_tokens = 4096
- # 3.5 and 3.7 models have a higher token limit
- max_tokens = 8192 if mapped_model.match?(/3.[57]/)
-
- options = { model: mapped_model, max_tokens: max_tokens }
-
- # reasoning has even higher token limits
- if llm_model.lookup_custom_param("enable_reasoning")
- reasoning_tokens =
- llm_model.lookup_custom_param("reasoning_tokens").to_i.clamp(1024, 32_768)
-
- # this allows for lots of tokens beyond reasoning
- options[:max_tokens] = reasoning_tokens + 30_000
- options[:thinking] = { type: "enabled", budget_tokens: reasoning_tokens }
- end
-
- options[:stop_sequences] = [""] if !dialect.native_tool_support? &&
- dialect.prompt.has_tools?
-
- options
- end
-
- def provider_id
- AiApiAuditLog::Provider::Anthropic
- end
-
- private
-
- def xml_tags_to_strip(dialect)
- if dialect.prompt.has_tools?
- %w[thinking search_quality_reflection search_quality_score]
- else
- []
- end
- end
-
- # this is an approximation, we will update it later if request goes through
- def prompt_size(prompt)
- tokenizer.size(prompt.system_prompt.to_s + " " + prompt.messages.to_s)
- end
-
- def model_uri
- URI(llm_model.url)
- end
-
- def xml_tools_enabled?
- !@native_tool_support
- end
-
- def prepare_payload(prompt, model_params, dialect)
- @native_tool_support = dialect.native_tool_support?
-
- payload =
- default_options(dialect).merge(model_params.except(:response_format)).merge(
- messages: prompt.messages,
- )
- payload[:system] = prompt.system_prompt if prompt.system_prompt.present?
- payload[:stream] = true if @streaming_mode
-
- prefilled_message = +""
-
- if prompt.has_tools?
- payload[:tools] = prompt.tools
- if dialect.tool_choice.present?
- if dialect.tool_choice == :none
- payload[:tool_choice] = { type: "none" }
-
- # prefill prompt to nudge LLM to generate a response that is useful.
- # without this LLM (even 3.7) can get confused and start text preambles for a tool calls.
- prefilled_message << dialect.no_more_tool_calls_text
- else
- payload[:tool_choice] = { type: "tool", name: prompt.tool_choice }
- end
- end
- end
-
- # Prefill prompt to force JSON output.
- if model_params[:response_format].present?
- prefilled_message << " " if !prefilled_message.empty?
- prefilled_message << "{"
- @forced_json_through_prefill = true
- end
-
- if !prefilled_message.empty?
- payload[:messages] << { role: "assistant", content: prefilled_message }
- end
-
- payload
- end
-
- def prepare_request(payload)
- headers = {
- "anthropic-version" => "2023-06-01",
- "x-api-key" => llm_model.api_key,
- "content-type" => "application/json",
- }
-
- Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
- end
-
- def decode_chunk(partial_data)
- @decoder ||= JsonStreamDecoder.new
- (@decoder << partial_data)
- .map { |parsed_json| processor.process_streamed_message(parsed_json) }
- .compact
- end
-
- def decode(response_data)
- processor.process_message(response_data)
- end
-
- def processor
- @processor ||=
- DiscourseAi::Completions::AnthropicMessageProcessor.new(
- streaming_mode: @streaming_mode,
- partial_tool_calls: partial_tool_calls,
- output_thinking: output_thinking,
- )
- end
-
- def has_tool?(_response_data)
- processor.tool_calls.present?
- end
-
- def tool_calls
- processor.to_tool_calls
- end
-
- def final_log_update(log)
- log.request_tokens = processor.input_tokens if processor.input_tokens
- log.response_tokens = processor.output_tokens if processor.output_tokens
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/aws_bedrock.rb b/lib/completions/endpoints/aws_bedrock.rb
deleted file mode 100644
index f1344f3a..00000000
--- a/lib/completions/endpoints/aws_bedrock.rb
+++ /dev/null
@@ -1,266 +0,0 @@
-# frozen_string_literal: true
-
-require "aws-sigv4"
-
-module DiscourseAi
- module Completions
- module Endpoints
- class AwsBedrock < Base
- attr_reader :dialect
-
- def self.can_contact?(model_provider)
- model_provider == "aws_bedrock"
- end
-
- def normalize_model_params(model_params)
- model_params = model_params.dup
-
- # max_tokens, temperature, stop_sequences, top_p are already supported
- #
- model_params.delete(:top_p) if llm_model.lookup_custom_param("disable_top_p")
- model_params.delete(:temperature) if llm_model.lookup_custom_param("disable_temperature")
-
- model_params
- end
-
- def default_options(dialect)
- options =
- if dialect.is_a?(DiscourseAi::Completions::Dialects::Claude)
- max_tokens = 4096
- max_tokens = 8192 if bedrock_model_id.match?(/3.[57]/)
-
- result = { anthropic_version: "bedrock-2023-05-31" }
- if llm_model.lookup_custom_param("enable_reasoning")
- # we require special headers to go over 64k output tokens, lets
- # wait for feature requests before enabling this
- reasoning_tokens =
- llm_model.lookup_custom_param("reasoning_tokens").to_i.clamp(1024, 32_768)
-
- # this allows for ample tokens beyond reasoning
- max_tokens = reasoning_tokens + 30_000
- result[:thinking] = { type: "enabled", budget_tokens: reasoning_tokens }
- end
- result[:max_tokens] = max_tokens
-
- result
- else
- {}
- end
-
- options[:stop_sequences] = [""] if !dialect.native_tool_support? &&
- dialect.prompt.has_tools?
- options
- end
-
- def provider_id
- AiApiAuditLog::Provider::Anthropic
- end
-
- def xml_tags_to_strip(dialect)
- if dialect.prompt.has_tools?
- %w[thinking search_quality_reflection search_quality_score]
- else
- []
- end
- end
-
- private
-
- def bedrock_model_id
- case llm_model.name
- when "claude-2"
- "anthropic.claude-v2:1"
- when "claude-3-haiku"
- "anthropic.claude-3-haiku-20240307-v1:0"
- when "claude-3-sonnet"
- "anthropic.claude-3-sonnet-20240229-v1:0"
- when "claude-instant-1"
- "anthropic.claude-instant-v1"
- when "claude-3-opus"
- "anthropic.claude-3-opus-20240229-v1:0"
- when "claude-3-5-sonnet"
- "anthropic.claude-3-5-sonnet-20241022-v2:0"
- when "claude-3-5-haiku"
- "anthropic.claude-3-5-haiku-20241022-v1:0"
- when "claude-3-7-sonnet"
- "anthropic.claude-3-7-sonnet-20250219-v1:0"
- else
- llm_model.name
- end
- end
-
- def prompt_size(prompt)
- # approximation
- tokenizer.size(prompt.system_prompt.to_s + " " + prompt.messages.to_s)
- end
-
- def model_uri
- region = llm_model.lookup_custom_param("region")
-
- if region.blank? || bedrock_model_id.blank?
- raise CompletionFailed.new(I18n.t("discourse_ai.llm_models.bedrock_invalid_url"))
- end
-
- api_url =
- "https://bedrock-runtime.#{region}.amazonaws.com/model/#{bedrock_model_id}/invoke"
-
- api_url = @streaming_mode ? (api_url + "-with-response-stream") : api_url
-
- URI(api_url)
- end
-
- def prepare_payload(prompt, model_params, dialect)
- @native_tool_support = dialect.native_tool_support?
- @dialect = dialect
-
- payload = nil
-
- if dialect.is_a?(DiscourseAi::Completions::Dialects::Claude)
- payload =
- default_options(dialect).merge(model_params.except(:response_format)).merge(
- messages: prompt.messages,
- )
-
- payload[:system] = prompt.system_prompt if prompt.system_prompt.present?
-
- prefilled_message = +""
-
- if prompt.has_tools?
- payload[:tools] = prompt.tools
- if dialect.tool_choice.present?
- if dialect.tool_choice == :none
- # not supported on bedrock as of 2025-03-24
- # retest in 6 months
- # payload[:tool_choice] = { type: "none" }
-
- # prefill prompt to nudge LLM to generate a response that is useful, instead of trying to call a tool
- prefilled_message << dialect.no_more_tool_calls_text
- else
- payload[:tool_choice] = { type: "tool", name: prompt.tool_choice }
- end
- end
- end
-
- # Prefill prompt to force JSON output.
- if model_params[:response_format].present?
- prefilled_message << " " if !prefilled_message.empty?
- prefilled_message << "{"
- @forced_json_through_prefill = true
- end
-
- if !prefilled_message.empty?
- payload[:messages] << { role: "assistant", content: prefilled_message }
- end
- elsif dialect.is_a?(DiscourseAi::Completions::Dialects::Nova)
- payload = prompt.to_payload(default_options(dialect).merge(model_params))
- else
- raise "Unsupported dialect"
- end
- payload
- end
-
- def prepare_request(payload)
- headers = { "content-type" => "application/json", "Accept" => "*/*" }
-
- signer =
- Aws::Sigv4::Signer.new(
- access_key_id: llm_model.lookup_custom_param("access_key_id"),
- region: llm_model.lookup_custom_param("region"),
- secret_access_key: llm_model.api_key,
- service: "bedrock",
- )
-
- Net::HTTP::Post
- .new(model_uri)
- .tap do |r|
- r.body = payload
-
- signed_request =
- signer.sign_request(req: r, http_method: r.method, url: model_uri, body: r.body)
-
- r.initialize_http_header(headers.merge(signed_request.headers))
- end
- end
-
- def decode_chunk(partial_data)
- bedrock_decode(partial_data)
- .map do |decoded_partial_data|
- @raw_response ||= +""
- @raw_response << decoded_partial_data
- @raw_response << "\n"
-
- parsed_json = JSON.parse(decoded_partial_data, symbolize_names: true)
- processor.process_streamed_message(parsed_json)
- end
- .compact
- end
-
- def decode(response_data)
- processor.process_message(response_data)
- end
-
- def bedrock_decode(chunk)
- @decoder ||= Aws::EventStream::Decoder.new
-
- decoded, _done = @decoder.decode_chunk(chunk)
-
- messages = []
- return messages if !decoded
-
- i = 0
- while decoded
- parsed = JSON.parse(decoded.payload.string)
- if exception = decoded.headers[":exception-type"]
- Rails.logger.error("#{self.class.name}: #{exception}: #{parsed}")
- # TODO based on how often this happens, we may want to raise so we
- # can retry, this may catch rate limits for example
- end
- # perhaps some control message we can just ignore
- messages << Base64.decode64(parsed["bytes"]) if parsed && parsed["bytes"]
-
- decoded, _done = @decoder.decode_chunk
-
- i += 1
- if i > 10_000
- Rails.logger.error(
- "DiscourseAI: Stream decoder looped too many times, logic error needs fixing",
- )
- break
- end
- end
-
- messages
- rescue JSON::ParserError,
- Aws::EventStream::Errors::MessageChecksumError,
- Aws::EventStream::Errors::PreludeChecksumError => e
- Rails.logger.error("#{self.class.name}: #{e.message}")
- []
- end
-
- def final_log_update(log)
- log.request_tokens = processor.input_tokens if processor.input_tokens
- log.response_tokens = processor.output_tokens if processor.output_tokens
- log.raw_response_payload = @raw_response if @raw_response
- end
-
- def processor
- if dialect.is_a?(DiscourseAi::Completions::Dialects::Claude)
- @processor ||=
- DiscourseAi::Completions::AnthropicMessageProcessor.new(
- streaming_mode: @streaming_mode,
- partial_tool_calls: partial_tool_calls,
- output_thinking: output_thinking,
- )
- else
- @processor ||=
- DiscourseAi::Completions::NovaMessageProcessor.new(streaming_mode: @streaming_mode)
- end
- end
-
- def xml_tools_enabled?
- !@native_tool_support
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/base.rb b/lib/completions/endpoints/base.rb
deleted file mode 100644
index abacdeec..00000000
--- a/lib/completions/endpoints/base.rb
+++ /dev/null
@@ -1,473 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class Base
- attr_reader :partial_tool_calls, :output_thinking
-
- CompletionFailed = Class.new(StandardError)
- # 6 minutes
- # Reasoning LLMs can take a very long time to respond, generally it will be under 5 minutes
- # The alternative is to have per LLM timeouts but that would make it extra confusing for people
- # configuring. Let's try this simple solution first.
- TIMEOUT = 360
-
- class << self
- def endpoint_for(provider_name)
- endpoints = [
- DiscourseAi::Completions::Endpoints::AwsBedrock,
- DiscourseAi::Completions::Endpoints::OpenAi,
- DiscourseAi::Completions::Endpoints::HuggingFace,
- DiscourseAi::Completions::Endpoints::Gemini,
- DiscourseAi::Completions::Endpoints::Vllm,
- DiscourseAi::Completions::Endpoints::Anthropic,
- DiscourseAi::Completions::Endpoints::Cohere,
- DiscourseAi::Completions::Endpoints::SambaNova,
- DiscourseAi::Completions::Endpoints::Mistral,
- DiscourseAi::Completions::Endpoints::OpenRouter,
- ]
-
- endpoints << DiscourseAi::Completions::Endpoints::Ollama if !Rails.env.production?
-
- if Rails.env.test? || Rails.env.development?
- endpoints << DiscourseAi::Completions::Endpoints::Fake
- end
-
- endpoints.detect(-> { raise DiscourseAi::Completions::Llm::UNKNOWN_MODEL }) do |ek|
- ek.can_contact?(provider_name)
- end
- end
-
- def can_contact?(_model_provider)
- raise NotImplementedError
- end
- end
-
- def initialize(llm_model)
- @llm_model = llm_model
- end
-
- def enforce_max_output_tokens(value)
- if @llm_model.max_output_tokens.to_i > 0
- value = @llm_model.max_output_tokens if (value.to_i > @llm_model.max_output_tokens) ||
- (value.to_i <= 0)
- end
- value
- end
-
- def use_ssl?
- if model_uri&.scheme.present?
- model_uri.scheme == "https"
- else
- true
- end
- end
-
- def xml_tags_to_strip(dialect)
- []
- end
-
- def perform_completion!(
- dialect,
- user,
- model_params = {},
- feature_name: nil,
- feature_context: nil,
- partial_tool_calls: false,
- output_thinking: false,
- cancel_manager: nil,
- &blk
- )
- LlmQuota.check_quotas!(@llm_model, user)
- start_time = Time.now
-
- if cancel_manager && cancel_manager.cancelled?
- # nothing to do
- return
- end
-
- @forced_json_through_prefill = false
- @partial_tool_calls = partial_tool_calls
- @output_thinking = output_thinking
-
- max_tokens = enforce_max_output_tokens(model_params[:max_tokens])
- model_params[:max_tokens] = max_tokens if max_tokens
- model_params = normalize_model_params(model_params)
- orig_blk = blk
-
- if block_given? && disable_streaming?
- result =
- perform_completion!(
- dialect,
- user,
- model_params,
- feature_name: feature_name,
- feature_context: feature_context,
- partial_tool_calls: partial_tool_calls,
- output_thinking: output_thinking,
- cancel_manager: cancel_manager,
- )
-
- wrapped = result
- wrapped = [result] if !result.is_a?(Array)
- wrapped.each do |partial|
- blk.call(partial)
- break cancel_manager&.cancelled?
- end
- return result
- end
-
- @streaming_mode = block_given?
-
- prompt = dialect.translate
-
- structured_output = nil
-
- if model_params[:response_format].present?
- schema_properties =
- model_params[:response_format].dig(:json_schema, :schema, :properties)
-
- if schema_properties.present?
- structured_output = DiscourseAi::Completions::StructuredOutput.new(schema_properties)
- end
- end
-
- cancel_manager_callback = nil
- cancelled = false
-
- FinalDestination::HTTP.start(
- model_uri.host,
- model_uri.port,
- use_ssl: use_ssl?,
- read_timeout: TIMEOUT,
- open_timeout: TIMEOUT,
- write_timeout: TIMEOUT,
- ) do |http|
- if cancel_manager
- cancel_manager_callback =
- lambda do
- cancelled = true
- http.finish
- end
- cancel_manager.add_callback(cancel_manager_callback)
- end
- response_data = +""
- response_raw = +""
-
- # Needed to response token calculations. Cannot rely on response_data due to function buffering.
- partials_raw = +""
- request_body = prepare_payload(prompt, model_params, dialect).to_json
-
- request = prepare_request(request_body)
-
- # Some providers rely on prefill to return structured outputs, so the start
- # of the JSON won't be included in the response. Supply it to keep JSON valid.
- structured_output << +"{" if structured_output && @forced_json_through_prefill
-
- http.request(request) do |response|
- if response.code.to_i != 200
- Rails.logger.error(
- "#{self.class.name}: status: #{response.code.to_i} - body: #{response.body}",
- )
- raise CompletionFailed, response.body
- end
-
- xml_tool_processor =
- XmlToolProcessor.new(
- partial_tool_calls: partial_tool_calls,
- tool_definitions: dialect.prompt.tools,
- ) if xml_tools_enabled? && dialect.prompt.has_tools?
-
- to_strip = xml_tags_to_strip(dialect)
- xml_stripper =
- DiscourseAi::Completions::XmlTagStripper.new(to_strip) if to_strip.present?
-
- if @streaming_mode
- blk =
- lambda do |partial|
- if partial.is_a?(String)
- partial = xml_stripper << partial if xml_stripper && !partial.empty?
-
- if structured_output.present?
- structured_output << partial if !partial.empty?
- partial = structured_output
- end
- end
- orig_blk.call(partial) if partial
- end
- end
-
- log =
- start_log(
- provider_id: provider_id,
- request_body: request_body,
- dialect: dialect,
- prompt: prompt,
- user: user,
- feature_name: feature_name,
- feature_context: feature_context,
- )
-
- if !@streaming_mode
- response_data =
- non_streaming_response(
- response: response,
- xml_tool_processor: xml_tool_processor,
- xml_stripper: xml_stripper,
- partials_raw: partials_raw,
- response_raw: response_raw,
- structured_output: structured_output,
- )
- return response_data
- end
-
- begin
- response.read_body do |chunk|
- break if cancelled
-
- response_raw << chunk
-
- decode_chunk(chunk).each do |partial|
- break if cancelled
- partials_raw << partial.to_s
- response_data << partial if partial.is_a?(String)
- partials = [partial]
- if xml_tool_processor && partial.is_a?(String)
- partials = (xml_tool_processor << partial)
- break if xml_tool_processor.should_cancel?
- end
- partials.each { |inner_partial| blk.call(inner_partial) }
- end
- end
- end
- if xml_stripper
- stripped = xml_stripper.finish
- if stripped.present?
- response_data << stripped
- result = []
- result = (xml_tool_processor << stripped) if xml_tool_processor
- result.each { |partial| blk.call(partial) }
- end
- end
- xml_tool_processor.finish.each { |partial| blk.call(partial) } if xml_tool_processor
- decode_chunk_finish.each { |partial| blk.call(partial) }
-
- if structured_output
- structured_output.finish
- if structured_output.broken?
- # signal last partial output which will get parsed
- # by best effort json parser
- blk.call("")
- end
- end
- return response_data
- ensure
- if log
- log.raw_response_payload = response_raw
- final_log_update(log)
- log.response_tokens = tokenizer.size(partials_raw) if log.response_tokens.blank?
- log.created_at = start_time
- log.updated_at = Time.now
- log.duration_msecs = (Time.now - start_time) * 1000
- log.save!
- LlmQuota.log_usage(@llm_model, user, log.request_tokens, log.response_tokens)
- if Rails.env.development? && !ENV["DISCOURSE_AI_NO_DEBUG"]
- puts "#{self.class.name}: request_tokens #{log.request_tokens} response_tokens #{log.response_tokens}"
- end
- end
- if log && (logger = Thread.current[:llm_audit_log])
- call_data = <<~LOG
- #{self.class.name}: request_tokens #{log.request_tokens} response_tokens #{log.response_tokens}
- request:
- #{format_possible_json_payload(log.raw_request_payload)}
- response:
- #{response_data}
- LOG
- logger.info(call_data)
- end
- if log && (structured_logger = Thread.current[:llm_audit_structured_log])
- llm_request =
- begin
- JSON.parse(log.raw_request_payload)
- rescue StandardError
- log.raw_request_payload
- end
-
- # gemini puts passwords in query params
- # we don't want to log that
- structured_logger.log(
- "llm_call",
- args: {
- class: self.class.name,
- completion_url: request.uri.to_s.split("?")[0],
- request: llm_request,
- result: response_data,
- request_tokens: log.request_tokens,
- response_tokens: log.response_tokens,
- duration: log.duration_msecs,
- stream: @streaming_mode,
- },
- start_time: start_time.utc,
- end_time: Time.now.utc,
- )
- end
- end
- end
- rescue IOError, StandardError
- raise if !cancelled
- ensure
- if cancel_manager && cancel_manager_callback
- cancel_manager.remove_callback(cancel_manager_callback)
- end
- end
-
- def final_log_update(log)
- # for people that need to override
- end
-
- def default_options
- raise NotImplementedError
- end
-
- def provider_id
- raise NotImplementedError
- end
-
- def prompt_size(prompt)
- tokenizer.size(extract_prompt_for_tokenizer(prompt))
- end
-
- attr_reader :llm_model
-
- protected
-
- def tokenizer
- llm_model.tokenizer_class
- end
-
- # should normalize temperature, max_tokens, stop_words to endpoint specific values
- def normalize_model_params(model_params)
- raise NotImplementedError
- end
-
- def model_uri
- raise NotImplementedError
- end
-
- def prepare_payload(_prompt, _model_params)
- raise NotImplementedError
- end
-
- def prepare_request(_payload)
- raise NotImplementedError
- end
-
- def decode(_response_raw)
- raise NotImplementedError
- end
-
- def decode_chunk_finish
- []
- end
-
- def decode_chunk(_chunk)
- raise NotImplementedError
- end
-
- def extract_prompt_for_tokenizer(prompt)
- prompt.map { |message| message[:content] || message["content"] || "" }.join("\n")
- end
-
- def xml_tools_enabled?
- raise NotImplementedError
- end
-
- def disable_streaming?
- @disable_streaming = !!llm_model.lookup_custom_param("disable_streaming")
- end
-
- private
-
- def format_possible_json_payload(payload)
- begin
- JSON.pretty_generate(JSON.parse(payload))
- rescue JSON::ParserError
- payload
- end
- end
-
- def start_log(
- provider_id:,
- request_body:,
- dialect:,
- prompt:,
- user:,
- feature_name:,
- feature_context:
- )
- AiApiAuditLog.new(
- provider_id: provider_id,
- user_id: user&.id,
- raw_request_payload: request_body,
- request_tokens: prompt_size(prompt),
- topic_id: dialect.prompt.topic_id,
- post_id: dialect.prompt.post_id,
- feature_name: feature_name,
- language_model: llm_model.name,
- feature_context: feature_context.present? ? feature_context.as_json : nil,
- )
- end
-
- def non_streaming_response(
- response:,
- xml_tool_processor:,
- xml_stripper:,
- partials_raw:,
- response_raw:,
- structured_output:
- )
- response_raw << response.read_body
- response_data = decode(response_raw)
-
- response_data.each { |partial| partials_raw << partial.to_s }
-
- if xml_tool_processor
- response_data.each do |partial|
- processed = (xml_tool_processor << partial)
- processed << xml_tool_processor.finish
- response_data = []
- processed.flatten.compact.each { |inner| response_data << inner }
- end
- end
-
- if xml_stripper
- response_data.map! do |partial|
- stripped = (xml_stripper << partial) if partial.is_a?(String)
- if stripped.present?
- stripped
- else
- partial
- end
- end
- response_data << xml_stripper.finish
- end
-
- response_data.reject!(&:blank?)
-
- if structured_output.present?
- response_data.each { |data| structured_output << data if data.is_a?(String) }
- structured_output.finish
-
- return structured_output
- end
-
- # this is to keep stuff backwards compatible
- response_data = response_data.first if response_data.length == 1
-
- response_data
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/canned_response.rb b/lib/completions/endpoints/canned_response.rb
deleted file mode 100644
index 9f2b10c3..00000000
--- a/lib/completions/endpoints/canned_response.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class CannedResponse
- CANNED_RESPONSE_ERROR = Class.new(StandardError)
-
- def initialize(responses)
- @responses = responses
- @completions = 0
- @dialect = nil
- end
-
- def normalize_model_params(model_params)
- # max_tokens, temperature, stop_sequences are already supported
- model_params
- end
-
- attr_reader :responses, :completions, :dialect, :model_params
-
- def prompt_messages
- dialect.prompt.messages
- end
-
- def perform_completion!(
- dialect,
- _user,
- model_params,
- feature_name: nil,
- feature_context: nil,
- partial_tool_calls: false,
- output_thinking: false,
- cancel_manager: nil
- )
- @dialect = dialect
- @model_params = model_params
- response = responses[completions]
- if response.nil?
- raise CANNED_RESPONSE_ERROR,
- "The number of completions you requested exceed the number of canned responses"
- end
-
- raise response if response.is_a?(StandardError)
-
- @completions += 1
- if block_given?
- cancelled = false
- cancel_fn = lambda { cancelled = true }
-
- # We buffer and return tool invocations in one go.
- as_array = response.is_a?(Array) ? response : [response]
- as_array.each do |response|
- if is_tool?(response)
- yield(response, cancel_fn)
- elsif is_thinking?(response)
- yield(response, cancel_fn)
- elsif model_params[:response_format].present?
- structured_output = as_structured_output(response)
- yield(structured_output, cancel_fn)
- else
- response.each_char do |char|
- break if cancelled
- yield(char, cancel_fn)
- end
- end
- end
- end
-
- response = response.first if response.is_a?(Array) && response.length == 1
- response = as_structured_output(response) if model_params[:response_format].present?
-
- response
- end
-
- def tokenizer
- DiscourseAi::Tokenizer::OpenAiTokenizer
- end
-
- private
-
- def is_thinking?(response)
- response.is_a?(DiscourseAi::Completions::Thinking)
- end
-
- def is_tool?(response)
- response.is_a?(DiscourseAi::Completions::ToolCall)
- end
-
- def as_structured_output(response)
- schema_properties = model_params[:response_format].dig(:json_schema, :schema, :properties)
- return response if schema_properties.blank?
-
- output = DiscourseAi::Completions::StructuredOutput.new(schema_properties)
- output << { schema_properties.keys.first => response }.to_json
-
- output
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/cohere.rb b/lib/completions/endpoints/cohere.rb
deleted file mode 100644
index 258062a1..00000000
--- a/lib/completions/endpoints/cohere.rb
+++ /dev/null
@@ -1,158 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class Cohere < Base
- def self.can_contact?(model_provider)
- model_provider == "cohere"
- end
-
- def normalize_model_params(model_params)
- model_params = model_params.dup
- model_params[:p] = model_params.delete(:top_p) if model_params[:top_p]
- model_params
- end
-
- def default_options(dialect)
- { model: "command-r-plus" }
- end
-
- def provider_id
- AiApiAuditLog::Provider::Cohere
- end
-
- private
-
- def model_uri
- URI(llm_model.url)
- end
-
- def prepare_payload(prompt, model_params, dialect)
- payload = default_options(dialect).merge(model_params).merge(prompt)
- if prompt[:tools].present?
- payload[:tools] = prompt[:tools]
- payload[:force_single_step] = false
- end
- payload[:tool_results] = prompt[:tool_results] if prompt[:tool_results].present?
- payload[:stream] = true if @streaming_mode
-
- payload
- end
-
- def prepare_request(payload)
- headers = {
- "Content-Type" => "application/json",
- "Authorization" => "Bearer #{llm_model.api_key}",
- }
-
- Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
- end
-
- def decode(response_raw)
- rval = []
-
- parsed = JSON.parse(response_raw, symbolize_names: true)
-
- text = parsed[:text]
- rval << parsed[:text] if !text.to_s.empty? # also allow " "
-
- # TODO tool calls
-
- update_usage(parsed)
-
- rval
- end
-
- def decode_chunk(chunk)
- @tool_idx ||= -1
- @json_decoder ||= JsonStreamDecoder.new(line_regex: /^\s*({.*})$/)
- (@json_decoder << chunk)
- .map do |parsed|
- update_usage(parsed)
- rval = []
-
- rval << parsed[:text] if !parsed[:text].to_s.empty?
-
- if tool_calls = parsed[:tool_calls]
- tool_calls&.each do |tool_call|
- @tool_idx += 1
- tool_name = tool_call[:name]
- tool_params = tool_call[:parameters]
- tool_id = "tool_#{@tool_idx}"
- rval << ToolCall.new(id: tool_id, name: tool_name, parameters: tool_params)
- end
- end
-
- rval
- end
- .flatten
- .compact
- end
-
- def extract_completion_from(response_raw)
- parsed = JSON.parse(response_raw, symbolize_names: true)
-
- if @streaming_mode
- if parsed[:event_type] == "text-generation"
- parsed[:text]
- elsif parsed[:event_type] == "tool-calls-generation"
- # could just be random thinking...
- if parsed.dig(:tool_calls).present?
- @has_tool = true
- parsed.dig(:tool_calls).to_json
- else
- ""
- end
- else
- if parsed[:event_type] == "stream-end"
- @input_tokens = parsed.dig(:response, :meta, :billed_units, :input_tokens)
- @output_tokens = parsed.dig(:response, :meta, :billed_units, :output_tokens)
- end
- nil
- end
- else
- @input_tokens = parsed.dig(:meta, :billed_units, :input_tokens)
- @output_tokens = parsed.dig(:meta, :billed_units, :output_tokens)
- parsed[:text].to_s
- end
- end
-
- def xml_tools_enabled?
- false
- end
-
- def final_log_update(log)
- log.request_tokens = @input_tokens if @input_tokens
- log.response_tokens = @output_tokens if @output_tokens
- end
-
- def extract_prompt_for_tokenizer(prompt)
- text = +""
- if prompt[:chat_history]
- text << prompt[:chat_history]
- .map { |message| message[:content] || message["content"] || "" }
- .join("\n")
- end
-
- text << prompt[:message] if prompt[:message]
- text << prompt[:preamble] if prompt[:preamble]
-
- text
- end
-
- private
-
- def update_usage(parsed)
- input_tokens = parsed.dig(:meta, :billed_units, :input_tokens)
- input_tokens ||= parsed.dig(:response, :meta, :billed_units, :input_tokens)
- @input_tokens = input_tokens if input_tokens.present?
-
- output_tokens = parsed.dig(:meta, :billed_units, :output_tokens)
- output_tokens ||= parsed.dig(:response, :meta, :billed_units, :output_tokens)
- @output_tokens = output_tokens if output_tokens.present?
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/fake.rb b/lib/completions/endpoints/fake.rb
deleted file mode 100644
index 71d0ee4b..00000000
--- a/lib/completions/endpoints/fake.rb
+++ /dev/null
@@ -1,175 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class Fake < Base
- STOCK_CONTENT = <<~TEXT
- # Discourse Markdown Styles Showcase
-
- Welcome to the **Discourse Markdown Styles Showcase**! This _post_ is designed to demonstrate a wide range of Markdown capabilities available in Discourse.
-
- ## Lists and Emphasis
-
- - **Bold Text**: To emphasize a point, you can use bold text.
- - _Italic Text_: To subtly highlight text, italics are perfect.
- - ~~Strikethrough~~: Sometimes, marking text as obsolete requires a strikethrough.
-
- > **Note**: Combining these _styles_ can **_really_** make your text stand out!
-
- 1. First item
- 2. Second item
- * Nested bullet
- * Another nested bullet
- 3. Third item
-
- ## Links and Images
-
- You can easily add [links](https://meta.discourse.org) to your posts. For adding images, use this syntax:
-
- 
-
- ## Code and Quotes
-
- Inline `code` is used for mentioning small code snippets like `let x = 10;`. For larger blocks of code, fenced code blocks are used:
-
- ```javascript
- function greet() {
- console.log("Hello, Discourse Community!");
- }
- greet();
- ```
-
- > Blockquotes can be very effective for highlighting user comments or important sections from cited sources. They stand out visually and offer great readability.
-
- ## Tables and Horizontal Rules
-
- Creating tables in Markdown is straightforward:
-
- | Header 1 | Header 2 | Header 3 |
- | ---------|:--------:| --------:|
- | Row 1, Col 1 | Centered | Right-aligned |
- | Row 2, Col 1 | **Bold** | _Italic_ |
- | Row 3, Col 1 | `Inline Code` | [Link](https://meta.discourse.org) |
-
- To separate content sections:
-
- ---
-
- ## Final Thoughts
-
- Congratulations, you've now seen a small sample of what Discourse's Markdown can do! For more intricate formatting, consider exploring the advanced styling options. Remember that the key to great formatting is not just the available tools, but also the **clarity** and **readability** it brings to your readers.
- TEXT
-
- def self.can_contact?(model_provider)
- model_provider == "fake"
- end
-
- def self.with_fake_content(content)
- @fake_content = content
- yield
- ensure
- @fake_content = nil
- end
-
- def self.fake_content=(content)
- @fake_content = content
- end
-
- def self.fake_content
- @fake_content || STOCK_CONTENT
- end
-
- def self.delays
- @delays ||= Array.new(10) { Rails.env.test? ? 0 : rand(0..5) }
- end
-
- def self.delays=(delays)
- @delays = delays
- end
-
- def self.chunk_count
- @chunk_count ||= 10
- end
-
- def self.chunk_count=(chunk_count)
- @chunk_count = chunk_count
- end
-
- def self.last_call
- @last_call
- end
-
- def self.last_call=(params)
- @last_call = params
- end
-
- def self.previous_calls
- @previous_calls ||= []
- end
-
- def self.reset!
- @last_call = nil
- @fake_content = nil
- @delays = nil
- @chunk_count = nil
- end
-
- def perform_completion!(
- dialect,
- user,
- model_params = {},
- feature_name: nil,
- feature_context: nil,
- partial_tool_calls: false,
- output_thinking: false,
- cancel_manager: nil
- )
- last_call = { dialect: dialect, user: user, model_params: model_params }
- self.class.last_call = last_call
- self.class.previous_calls << last_call
- # guard memory in test
- self.class.previous_calls.shift if self.class.previous_calls.length > 10
-
- content = self.class.fake_content
-
- content = content.shift if content.is_a?(Array)
-
- if block_given?
- if content.is_a?(DiscourseAi::Completions::ToolCall)
- yield(content, -> {})
- else
- split_indices = (1...content.length).to_a.sample(self.class.chunk_count - 1).sort
- indexes = [0, *split_indices, content.length]
-
- original_content = content
- content = +""
-
- cancel = false
- cancel_proc = -> { cancel = true }
-
- i = 0
- indexes
- .each_cons(2)
- .map { |start, finish| original_content[start...finish] }
- .each do |chunk|
- break if cancel
- if self.class.delays.present? &&
- (delay = self.class.delays[i % self.class.delays.length])
- sleep(delay)
- i += 1
- end
- break if cancel
-
- content << chunk
- yield(chunk, cancel_proc)
- end
- end
- end
-
- content
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/gemini.rb b/lib/completions/endpoints/gemini.rb
deleted file mode 100644
index f5b1cb19..00000000
--- a/lib/completions/endpoints/gemini.rb
+++ /dev/null
@@ -1,257 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class Gemini < Base
- def self.can_contact?(model_provider)
- model_provider == "google"
- end
-
- def default_options
- # the default setting is a problem, it blocks too much
- categories = %w[HARASSMENT SEXUALLY_EXPLICIT HATE_SPEECH DANGEROUS_CONTENT]
-
- safety_settings =
- categories.map do |category|
- { category: "HARM_CATEGORY_#{category}", threshold: "BLOCK_NONE" }
- end
-
- { generationConfig: {}, safetySettings: safety_settings }
- end
-
- def normalize_model_params(model_params)
- model_params = model_params.dup
-
- if model_params[:stop_sequences]
- model_params[:stopSequences] = model_params.delete(:stop_sequences)
- end
-
- if model_params[:max_tokens]
- model_params[:maxOutputTokens] = model_params.delete(:max_tokens)
- end
-
- model_params[:topP] = model_params.delete(:top_p) if model_params[:top_p]
-
- model_params.delete(:temperature) if llm_model.lookup_custom_param("disable_temperature")
- model_params.delete(:topP) if llm_model.lookup_custom_param("disable_top_p")
-
- model_params
- end
-
- def provider_id
- AiApiAuditLog::Provider::Gemini
- end
-
- private
-
- def model_uri
- url = llm_model.url
- key = llm_model.api_key
-
- if @streaming_mode
- url = "#{url}:streamGenerateContent?key=#{key}&alt=sse"
- else
- url = "#{url}:generateContent?key=#{key}"
- end
-
- URI(url)
- end
-
- def prepare_payload(prompt, model_params, dialect)
- @native_tool_support = dialect.native_tool_support?
-
- tools = dialect.tools if @native_tool_support
-
- payload = default_options.merge(contents: prompt[:messages])
-
- payload[:systemInstruction] = {
- role: "system",
- parts: [{ text: prompt[:system_instruction].to_s }],
- } if prompt[:system_instruction].present?
- if tools.present?
- payload[:tools] = tools
-
- function_calling_config = { mode: "AUTO" }
- if dialect.tool_choice.present?
- if dialect.tool_choice == :none
- function_calling_config = { mode: "NONE" }
- else
- function_calling_config = {
- mode: "ANY",
- allowed_function_names: [dialect.tool_choice],
- }
- end
- end
-
- payload[:tool_config] = { function_calling_config: function_calling_config }
- end
- if model_params.present?
- payload[:generationConfig].merge!(model_params.except(:response_format))
-
- # https://ai.google.dev/api/generate-content#generationconfig
- gemini_schema = model_params.dig(:response_format, :json_schema, :schema)
-
- if gemini_schema.present?
- payload[:generationConfig][:responseSchema] = gemini_schema.except(
- :additionalProperties,
- )
- payload[:generationConfig][:responseMimeType] = "application/json"
- end
- end
-
- if llm_model.lookup_custom_param("enable_thinking")
- thinking_tokens = llm_model.lookup_custom_param("thinking_tokens").to_i
- thinking_tokens = thinking_tokens.clamp(0, 24_576)
- payload[:generationConfig][:thinkingConfig] = { thinkingBudget: thinking_tokens }
- end
-
- payload
- end
-
- def prepare_request(payload)
- headers = { "Content-Type" => "application/json" }
-
- Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
- end
-
- def extract_completion_from(response_raw)
- parsed =
- if @streaming_mode
- response_raw
- else
- JSON.parse(response_raw, symbolize_names: true)
- end
- response_h = parsed.dig(:candidates, 0, :content, :parts, 0)
-
- if response_h
- @has_function_call ||= response_h.dig(:functionCall).present?
- @has_function_call ? response_h.dig(:functionCall) : response_h.dig(:text)
- end
- end
-
- class GeminiStreamingDecoder
- def initialize
- @buffer = +""
- end
-
- def decode(str)
- @buffer << str
-
- lines = @buffer.split(/\r?\n\r?\n/)
-
- keep_last = false
-
- decoded =
- lines
- .map do |line|
- if line.start_with?("data: {")
- begin
- JSON.parse(line[6..-1], symbolize_names: true)
- rescue JSON::ParserError
- keep_last = line
- nil
- end
- else
- keep_last = line
- nil
- end
- end
- .compact
-
- if keep_last
- @buffer = +(keep_last)
- else
- @buffer = +""
- end
-
- decoded
- end
- end
-
- def decode(chunk)
- json = JSON.parse(chunk, symbolize_names: true)
-
- idx = -1
- json
- .dig(:candidates, 0, :content, :parts)
- .map do |part|
- if part[:functionCall]
- idx += 1
- ToolCall.new(
- id: "tool_#{idx}",
- name: part[:functionCall][:name],
- parameters: part[:functionCall][:args],
- )
- else
- part = part[:text]
- if part != ""
- part
- else
- nil
- end
- end
- end
- end
-
- def decode_chunk(chunk)
- @tool_index ||= -1
- streaming_decoder
- .decode(chunk)
- .map do |parsed|
- update_usage(parsed)
- parts = parsed.dig(:candidates, 0, :content, :parts)
- parts&.map do |part|
- if part[:text]
- part = part[:text]
- if part != ""
- part
- else
- nil
- end
- elsif part[:functionCall]
- @tool_index += 1
- ToolCall.new(
- id: "tool_#{@tool_index}",
- name: part[:functionCall][:name],
- parameters: part[:functionCall][:args],
- )
- end
- end
- end
- .flatten
- .compact
- end
-
- def update_usage(parsed)
- usage = parsed.dig(:usageMetadata)
- if usage
- if prompt_token_count = usage[:promptTokenCount]
- @prompt_token_count = prompt_token_count
- end
- if candidate_token_count = usage[:candidatesTokenCount]
- @candidate_token_count = candidate_token_count
- end
- end
- end
-
- def final_log_update(log)
- log.request_tokens = @prompt_token_count if @prompt_token_count
- log.response_tokens = @candidate_token_count if @candidate_token_count
- end
-
- def streaming_decoder
- @decoder ||= GeminiStreamingDecoder.new
- end
-
- def extract_prompt_for_tokenizer(prompt)
- prompt.to_s
- end
-
- def xml_tools_enabled?
- !@native_tool_support
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/hugging_face.rb b/lib/completions/endpoints/hugging_face.rb
deleted file mode 100644
index b0b14722..00000000
--- a/lib/completions/endpoints/hugging_face.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class HuggingFace < Base
- def self.can_contact?(model_provider)
- model_provider == "hugging_face"
- end
-
- def normalize_model_params(model_params)
- model_params = model_params.dup
-
- # max_tokens, temperature are already supported
- if model_params[:stop_sequences]
- model_params[:stop] = model_params.delete(:stop_sequences)
- end
-
- model_params
- end
-
- def default_options
- { model: llm_model.name, temperature: 0.7 }
- end
-
- def provider_id
- AiApiAuditLog::Provider::HuggingFaceTextGeneration
- end
-
- private
-
- def model_uri
- URI(llm_model.url)
- end
-
- def prepare_payload(prompt, model_params, _dialect)
- default_options
- .merge(model_params)
- .merge(messages: prompt)
- .tap do |payload|
- if !payload[:max_tokens]
- token_limit = llm_model.max_prompt_tokens
-
- payload[:max_tokens] = token_limit - prompt_size(prompt)
- end
-
- payload[:stream] = true if @streaming_mode
- end
- end
-
- def prepare_request(payload)
- api_key = llm_model.api_key
-
- headers =
- { "Content-Type" => "application/json" }.tap do |h|
- h["Authorization"] = "Bearer #{api_key}" if api_key.present?
- end
-
- Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
- end
-
- def xml_tools_enabled?
- true
- end
-
- def decode(response_raw)
- parsed = JSON.parse(response_raw, symbolize_names: true)
- text = parsed.dig(:choices, 0, :message, :content)
- if text.to_s.empty?
- [""]
- else
- [text]
- end
- end
-
- def decode_chunk(chunk)
- @json_decoder ||= JsonStreamDecoder.new
- (@json_decoder << chunk)
- .map do |parsed|
- text = parsed.dig(:choices, 0, :delta, :content)
- if text.to_s.empty?
- nil
- else
- text
- end
- end
- .compact
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/mistral.rb b/lib/completions/endpoints/mistral.rb
deleted file mode 100644
index 5414b3df..00000000
--- a/lib/completions/endpoints/mistral.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class Mistral < OpenAi
- def self.can_contact?(model_provider)
- model_provider == "mistral"
- end
-
- def provider_id
- AiApiAuditLog::Provider::Mistral
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/ollama.rb b/lib/completions/endpoints/ollama.rb
deleted file mode 100644
index dd4ca2c7..00000000
--- a/lib/completions/endpoints/ollama.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class Ollama < Base
- def self.can_contact?(model_provider)
- model_provider == "ollama"
- end
-
- def normalize_model_params(model_params)
- model_params = model_params.dup
-
- # max_tokens, temperature are already supported
- if model_params[:stop_sequences]
- model_params[:stop] = model_params.delete(:stop_sequences)
- end
-
- model_params
- end
-
- def default_options
- { max_tokens: 2000, model: llm_model.name }
- end
-
- def provider_id
- AiApiAuditLog::Provider::Ollama
- end
-
- def use_ssl?
- false
- end
-
- private
-
- def model_uri
- URI(llm_model.url)
- end
-
- def xml_tools_enabled?
- !@native_tool_support
- end
-
- def prepare_payload(prompt, model_params, dialect)
- @native_tool_support = dialect.native_tool_support?
-
- # https://github.com/ollama/ollama/blob/main/docs/api.md#parameters-1
- # Due to ollama enforce a 'stream: false' for tool calls, instead of complicating the code,
- # we will just disable streaming for all ollama calls if native tool support is enabled
-
- default_options
- .merge(model_params)
- .merge(messages: prompt)
- .tap { |payload| payload[:stream] = false if @native_tool_support || !@streaming_mode }
- .tap do |payload|
- payload[:tools] = dialect.tools if @native_tool_support && dialect.tools.present?
- end
- end
-
- def prepare_request(payload)
- headers = { "Content-Type" => "application/json" }
-
- Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
- end
-
- def decode_chunk(chunk)
- # Native tool calls are not working right in streaming mode, use XML
- @json_decoder ||= JsonStreamDecoder.new(line_regex: /^\s*({.*})$/)
- (@json_decoder << chunk).map { |parsed| parsed.dig(:message, :content) }.compact
- end
-
- def decode(response_raw)
- rval = []
- parsed = JSON.parse(response_raw, symbolize_names: true)
- content = parsed.dig(:message, :content)
- rval << content if !content.to_s.empty?
-
- idx = -1
- parsed
- .dig(:message, :tool_calls)
- &.each do |tool_call|
- idx += 1
- id = "tool_#{idx}"
- name = tool_call.dig(:function, :name)
- args = tool_call.dig(:function, :arguments)
- rval << ToolCall.new(id: id, name: name, parameters: args)
- end
-
- rval
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/open_ai.rb b/lib/completions/endpoints/open_ai.rb
deleted file mode 100644
index 36e49a4a..00000000
--- a/lib/completions/endpoints/open_ai.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class OpenAi < Base
- def self.can_contact?(model_provider)
- %w[open_ai azure].include?(model_provider)
- end
-
- def normalize_model_params(model_params)
- model_params = model_params.dup
-
- # max_tokens is deprecated however we still need to support it
- # on older OpenAI models and older Azure models, so we will only normalize
- # if our model name starts with o (to denote all the reasoning models)
- if llm_model.name.starts_with?("o")
- max_tokens = model_params.delete(:max_tokens)
- model_params[:max_completion_tokens] = max_tokens if max_tokens
- end
-
- # temperature is already supported
- if model_params[:stop_sequences]
- model_params[:stop] = model_params.delete(:stop_sequences)
- end
-
- model_params.delete(:top_p) if llm_model.lookup_custom_param("disable_top_p")
- model_params.delete(:temperature) if llm_model.lookup_custom_param("disable_temperature")
-
- model_params
- end
-
- def default_options
- { model: llm_model.name }
- end
-
- def provider_id
- AiApiAuditLog::Provider::OpenAI
- end
-
- def perform_completion!(
- dialect,
- user,
- model_params = {},
- feature_name: nil,
- feature_context: nil,
- partial_tool_calls: false,
- output_thinking: false,
- cancel_manager: nil,
- &blk
- )
- @disable_native_tools = dialect.disable_native_tools?
- super
- end
-
- private
-
- def disable_streaming?
- @disable_streaming ||= llm_model.lookup_custom_param("disable_streaming")
- end
-
- def reasoning_effort
- return @reasoning_effort if defined?(@reasoning_effort)
- @reasoning_effort = llm_model.lookup_custom_param("reasoning_effort")
- @reasoning_effort = nil if !%w[low medium high].include?(@reasoning_effort)
- @reasoning_effort
- end
-
- def model_uri
- if llm_model.url.to_s.starts_with?("srv://")
- service = DiscourseAi::Utils::DnsSrv.lookup(llm_model.url.sub("srv://", ""))
- api_endpoint = "https://#{service.target}:#{service.port}/v1/chat/completions"
- else
- api_endpoint = llm_model.url
- end
-
- @uri ||= URI(api_endpoint)
- end
-
- def prepare_payload(prompt, model_params, dialect)
- payload = default_options.merge(model_params).merge(messages: prompt)
-
- payload[:reasoning_effort] = reasoning_effort if reasoning_effort
-
- if @streaming_mode
- payload[:stream] = true
-
- # Usage is not available in Azure yet.
- # We'll fallback to guess this using the tokenizer.
- payload[:stream_options] = { include_usage: true } if llm_model.provider == "open_ai"
- end
-
- if !xml_tools_enabled?
- if dialect.tools.present?
- payload[:tools] = dialect.tools
- if dialect.tool_choice.present?
- if dialect.tool_choice == :none
- payload[:tool_choice] = "none"
- else
- if responses_api?
- payload[:tool_choice] = { type: "function", name: dialect.tool_choice }
- else
- payload[:tool_choice] = {
- type: "function",
- function: {
- name: dialect.tool_choice,
- },
- }
- end
- end
- end
- end
- end
-
- convert_payload_to_responses_api!(payload) if responses_api?
-
- payload
- end
-
- def responses_api?
- return @responses_api if defined?(@responses_api)
- @responses_api = llm_model.lookup_custom_param("enable_responses_api")
- end
-
- def convert_payload_to_responses_api!(payload)
- payload[:input] = payload.delete(:messages)
- completion_tokens = payload.delete(:max_completion_tokens) || payload.delete(:max_tokens)
- payload[:max_output_tokens] = completion_tokens if completion_tokens
- # not supported in responses api
- payload.delete(:stream_options)
- end
-
- def prepare_request(payload)
- headers = { "Content-Type" => "application/json" }
- api_key = llm_model.api_key
-
- if llm_model.provider == "azure"
- headers["api-key"] = api_key
- else
- headers["Authorization"] = "Bearer #{api_key}"
- org_id = llm_model.lookup_custom_param("organization")
- headers["OpenAI-Organization"] = org_id if org_id.present?
- end
-
- Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
- end
-
- def final_log_update(log)
- log.request_tokens = processor.prompt_tokens if processor.prompt_tokens
- log.response_tokens = processor.completion_tokens if processor.completion_tokens
- log.cached_tokens = processor.cached_tokens if processor.cached_tokens
- end
-
- def decode(response_raw)
- processor.process_message(JSON.parse(response_raw, symbolize_names: true))
- end
-
- def decode_chunk(chunk)
- @decoder ||= JsonStreamDecoder.new
- elements =
- (@decoder << chunk)
- .map { |parsed_json| processor.process_streamed_message(parsed_json) }
- .flatten
- .compact
-
- # Remove duplicate partial tool calls
- # sometimes we stream weird chunks
- seen_tools = Set.new
- elements.select { |item| !item.is_a?(ToolCall) || seen_tools.add?(item) }
- end
-
- def decode_chunk_finish
- processor.finish
- end
-
- def xml_tools_enabled?
- !!@disable_native_tools
- end
-
- private
-
- def processor
- @processor ||=
- if responses_api?
- OpenAiResponsesMessageProcessor.new(partial_tool_calls: partial_tool_calls)
- else
- OpenAiMessageProcessor.new(partial_tool_calls: partial_tool_calls)
- end
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/open_router.rb b/lib/completions/endpoints/open_router.rb
deleted file mode 100644
index 08122799..00000000
--- a/lib/completions/endpoints/open_router.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class OpenRouter < OpenAi
- def self.can_contact?(model_provider)
- %w[open_router].include?(model_provider)
- end
-
- def normalize_model_params(model_params)
- model_params = model_params.dup
-
- # max_tokens, temperature are already supported
- if model_params[:stop_sequences]
- model_params[:stop] = model_params.delete(:stop_sequences)
- end
-
- model_params.delete(:top_p) if llm_model.lookup_custom_param("disable_top_p")
- model_params.delete(:temperature) if llm_model.lookup_custom_param("disable_temperature")
-
- model_params
- end
-
- def prepare_request(payload)
- headers = { "Content-Type" => "application/json" }
- api_key = llm_model.api_key
-
- headers["Authorization"] = "Bearer #{api_key}"
- headers["X-Title"] = "Discourse AI"
- headers["HTTP-Referer"] = "https://www.discourse.org/ai"
-
- Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
- end
-
- def prepare_payload(prompt, model_params, dialect)
- payload = super
-
- if quantizations = llm_model.provider_params["provider_quantizations"].presence
- options = quantizations.split(",").map(&:strip)
-
- payload[:provider] = { quantizations: options }
- end
-
- if order = llm_model.provider_params["provider_order"].presence
- options = order.split(",").map(&:strip)
- payload[:provider] ||= {}
- payload[:provider][:order] = options
- end
-
- payload
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/samba_nova.rb b/lib/completions/endpoints/samba_nova.rb
deleted file mode 100644
index 9e6b3817..00000000
--- a/lib/completions/endpoints/samba_nova.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class SambaNova < Base
- def self.can_contact?(model_provider)
- model_provider == "samba_nova"
- end
-
- def normalize_model_params(model_params)
- model_params = model_params.dup
-
- # max_tokens, temperature are already supported
- if model_params[:stop_sequences]
- model_params[:stop] = model_params.delete(:stop_sequences)
- end
-
- model_params
- end
-
- def default_options
- { model: llm_model.name }
- end
-
- def provider_id
- AiApiAuditLog::Provider::SambaNova
- end
-
- private
-
- def model_uri
- URI(llm_model.url)
- end
-
- def prepare_payload(prompt, model_params, dialect)
- payload =
- default_options.merge(model_params.except(:response_format)).merge(messages: prompt)
-
- if model_params[:response_format].present?
- payload[:response_format] = { type: "json_object" }
- end
-
- payload[:stream] = true if @streaming_mode
-
- payload
- end
-
- def prepare_request(payload)
- headers = { "Content-Type" => "application/json" }
- api_key = llm_model.api_key
-
- headers["Authorization"] = "Bearer #{api_key}"
-
- Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
- end
-
- def final_log_update(log)
- log.request_tokens = @prompt_tokens if @prompt_tokens
- log.response_tokens = @completion_tokens if @completion_tokens
- end
-
- def xml_tools_enabled?
- true
- end
-
- def decode(response_raw)
- json = JSON.parse(response_raw, symbolize_names: true)
- [json.dig(:choices, 0, :message, :content)]
- end
-
- def decode_chunk(chunk)
- @json_decoder ||= JsonStreamDecoder.new
- (@json_decoder << chunk)
- .map do |json|
- text = json.dig(:choices, 0, :delta, :content)
-
- @prompt_tokens ||= json.dig(:usage, :prompt_tokens)
- @completion_tokens ||= json.dig(:usage, :completion_tokens)
-
- if !text.to_s.empty?
- text
- else
- nil
- end
- end
- .flatten
- .compact
- end
- end
- end
- end
-end
diff --git a/lib/completions/endpoints/vllm.rb b/lib/completions/endpoints/vllm.rb
deleted file mode 100644
index 6b371a09..00000000
--- a/lib/completions/endpoints/vllm.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- module Endpoints
- class Vllm < Base
- def self.can_contact?(model_provider)
- model_provider == "vllm"
- end
-
- def normalize_model_params(model_params)
- model_params = model_params.dup
-
- # max_tokens, temperature are already supported
- if model_params[:stop_sequences]
- model_params[:stop] = model_params.delete(:stop_sequences)
- end
-
- model_params
- end
-
- def default_options
- { max_tokens: 2000, model: llm_model.name }
- end
-
- def provider_id
- AiApiAuditLog::Provider::Vllm
- end
-
- private
-
- def model_uri
- if llm_model.url.to_s.starts_with?("srv://")
- service = DiscourseAi::Utils::DnsSrv.lookup(llm_model.url.sub("srv://", ""))
- api_endpoint = "https://#{service.target}:#{service.port}/v1/chat/completions"
- else
- api_endpoint = llm_model.url
- end
-
- @uri ||= URI(api_endpoint)
- end
-
- def prepare_payload(prompt, model_params, dialect)
- payload = default_options.merge(model_params).merge(messages: prompt)
- if @streaming_mode
- payload[:stream] = true if @streaming_mode
- payload[:stream_options] = { include_usage: true }
- end
-
- payload
- end
-
- def prepare_request(payload)
- headers = { "Referer" => Discourse.base_url, "Content-Type" => "application/json" }
-
- api_key = llm_model&.api_key || SiteSetting.ai_vllm_api_key
- headers["X-API-KEY"] = api_key if api_key.present?
-
- Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
- end
-
- def xml_tools_enabled?
- true
- end
-
- def final_log_update(log)
- log.request_tokens = @prompt_tokens if @prompt_tokens
- log.response_tokens = @completion_tokens if @completion_tokens
- end
-
- def decode(response_raw)
- json = JSON.parse(response_raw, symbolize_names: true)
- @prompt_tokens = json.dig(:usage, :prompt_tokens)
- @completion_tokens = json.dig(:usage, :completion_tokens)
- [json.dig(:choices, 0, :message, :content)]
- end
-
- def decode_chunk(chunk)
- @json_decoder ||= JsonStreamDecoder.new
- (@json_decoder << chunk)
- .map do |parsed|
- # vLLM keeps sending usage over and over again
- prompt_tokens = parsed.dig(:usage, :prompt_tokens)
- completion_tokens = parsed.dig(:usage, :completion_tokens)
-
- @prompt_tokens = prompt_tokens if prompt_tokens
-
- @completion_tokens = completion_tokens if completion_tokens
-
- text = parsed.dig(:choices, 0, :delta, :content)
- if text.to_s.empty?
- nil
- else
- text
- end
- end
- .compact
- end
- end
- end
- end
-end
diff --git a/lib/completions/json_stream_decoder.rb b/lib/completions/json_stream_decoder.rb
deleted file mode 100644
index e575a3b7..00000000
--- a/lib/completions/json_stream_decoder.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- # will work for anthropic and open ai compatible
- class JsonStreamDecoder
- attr_reader :buffer
-
- LINE_REGEX = /data: ({.*})\s*$/
-
- def initialize(symbolize_keys: true, line_regex: LINE_REGEX)
- @symbolize_keys = symbolize_keys
- @buffer = +""
- @line_regex = line_regex
- end
-
- def <<(raw)
- @buffer << raw.to_s
- rval = []
-
- split = @buffer.scan(/.*\n?/)
- split.pop if split.last.blank?
-
- @buffer = +(split.pop.to_s)
-
- split.each do |line|
- matches = line.match(@line_regex)
- next if !matches
- rval << JSON.parse(matches[1], symbolize_names: @symbolize_keys)
- end
-
- if @buffer.present?
- matches = @buffer.match(@line_regex)
- if matches
- begin
- rval << JSON.parse(matches[1], symbolize_names: @symbolize_keys)
- @buffer = +""
- rescue JSON::ParserError
- # maybe it is a partial line
- end
- end
- end
-
- rval
- end
- end
- end
-end
diff --git a/lib/completions/json_streaming_parser.rb b/lib/completions/json_streaming_parser.rb
deleted file mode 100644
index f3288ab6..00000000
--- a/lib/completions/json_streaming_parser.rb
+++ /dev/null
@@ -1,668 +0,0 @@
-# frozen_string_literal: true
-
-# This code is copied from the MIT licensed json-stream
-# see: https://github.com/dgraham/json-stream
-#
-# It was copied to avoid the dependency and allow us to make some small changes
-# particularly we need better access to internal state when parsing
-
-module DiscourseAi
- module Completions
- # Raised on any invalid JSON text.
- ParserError = Class.new(RuntimeError)
-
- # A streaming JSON parser that generates SAX-like events for state changes.
- # Use the json gem for small documents. Use this for huge documents that
- # won't fit in memory.
- #
- # Examples
- #
- # parser = JSON::Stream::Parser.new
- # parser.key { |key| puts key }
- # parser.value { |value| puts value }
- # parser << '{"answer":'
- # parser << ' 42}'
- class JsonStreamingParser
- # our changes:
- attr_reader :state, :buf, :pos
-
- # A character buffer that expects a UTF-8 encoded stream of bytes.
- # This handles truncated multi-byte characters properly so we can just
- # feed it binary data and receive a properly formatted UTF-8 String as
- # output.
- #
- # More UTF-8 parsing details are available at:
- #
- # http://en.wikipedia.org/wiki/UTF-8
- # http://tools.ietf.org/html/rfc3629#section-3
- class Buffer
- def initialize
- @state = :start
- @buffer = []
- @need = 0
- end
-
- # Fill the buffer with a String of binary UTF-8 encoded bytes. Returns
- # as much of the data in a UTF-8 String as we have. Truncated multi-byte
- # characters are saved in the buffer until the next call to this method
- # where we expect to receive the rest of the multi-byte character.
- #
- # data - The partial binary encoded String data.
- #
- # Raises JSON::Stream::ParserError if the UTF-8 byte sequence is malformed.
- #
- # Returns a UTF-8 encoded String.
- def <<(data)
- data = data.dup if data.frozen?
- # Avoid state machine for complete UTF-8.
- if @buffer.empty?
- data.force_encoding(Encoding::UTF_8)
- return data if data.valid_encoding?
- end
-
- bytes = []
- data.each_byte do |byte|
- case @state
- when :start
- if byte < 128
- bytes << byte
- elsif byte >= 192
- @state = :multi_byte
- @buffer << byte
- @need =
- case
- when byte >= 240
- 4
- when byte >= 224
- 3
- when byte >= 192
- 2
- end
- else
- error("Expected start of multi-byte or single byte char")
- end
- when :multi_byte
- if byte > 127 && byte < 192
- @buffer << byte
- if @buffer.size == @need
- bytes += @buffer.slice!(0, @buffer.size)
- @state = :start
- end
- else
- error("Expected continuation byte")
- end
- end
- end
-
- # Build UTF-8 encoded string from completed codepoints.
- bytes
- .pack("C*")
- .force_encoding(Encoding::UTF_8)
- .tap { |text| error("Invalid UTF-8 byte sequence") unless text.valid_encoding? }
- end
-
- # Determine if the buffer contains partial UTF-8 continuation bytes that
- # are waiting on subsequent completion bytes before a full codepoint is
- # formed.
- #
- # Examples
- #
- # bytes = "é".bytes
- #
- # buffer << bytes[0]
- # buffer.empty?
- # # => false
- #
- # buffer << bytes[1]
- # buffer.empty?
- # # => true
- #
- # Returns true if the buffer is empty.
- def empty?
- @buffer.empty?
- end
-
- private
-
- def error(message)
- raise ParserError, message
- end
- end
-
- BUF_SIZE = 4096
- CONTROL = /[\x00-\x1F]/
- WS = /[ \n\t\r]/
- HEX = /[0-9a-fA-F]/
- DIGIT = /[0-9]/
- DIGIT_1_9 = /[1-9]/
- DIGIT_END = /\d$/
- TRUE_RE = /[rue]/
- FALSE_RE = /[alse]/
- NULL_RE = /[ul]/
- TRUE_KEYWORD = "true"
- FALSE_KEYWORD = "false"
- NULL_KEYWORD = "null"
- LEFT_BRACE = "{"
- RIGHT_BRACE = "}"
- LEFT_BRACKET = "["
- RIGHT_BRACKET = "]"
- BACKSLASH = '\\'
- SLASH = "/"
- QUOTE = '"'
- COMMA = ","
- COLON = ":"
- ZERO = "0"
- MINUS = "-"
- PLUS = "+"
- POINT = "."
- EXPONENT = /[eE]/
- B, F, N, R, T, U = %w[b f n r t u]
-
- # Create a new parser with an optional initialization block where
- # we can register event callbacks.
- #
- # Examples
- #
- # parser = JSON::Stream::Parser.new do
- # start_document { puts "start document" }
- # end_document { puts "end document" }
- # start_object { puts "start object" }
- # end_object { puts "end object" }
- # start_array { puts "start array" }
- # end_array { puts "end array" }
- # key { |k| puts "key: #{k}" }
- # value { |v| puts "value: #{v}" }
- # end
- def initialize(&block)
- @state = :start_document
- @utf8 = Buffer.new
- @listeners = {
- start_document: [],
- end_document: [],
- start_object: [],
- end_object: [],
- start_array: [],
- end_array: [],
- key: [],
- value: [],
- }
-
- # Track parse stack.
- @stack = []
- @unicode = +""
- @buf = +""
- @pos = -1
-
- # Register any observers in the block.
- instance_eval(&block) if block_given?
- end
-
- def start_document(&block)
- @listeners[:start_document] << block
- end
-
- def end_document(&block)
- @listeners[:end_document] << block
- end
-
- def start_object(&block)
- @listeners[:start_object] << block
- end
-
- def end_object(&block)
- @listeners[:end_object] << block
- end
-
- def start_array(&block)
- @listeners[:start_array] << block
- end
-
- def end_array(&block)
- @listeners[:end_array] << block
- end
-
- def key(&block)
- @listeners[:key] << block
- end
-
- def value(&block)
- @listeners[:value] << block
- end
-
- # Pass data into the parser to advance the state machine and
- # generate callback events. This is well suited for an EventMachine
- # receive_data loop.
- #
- # data - The String of partial JSON data to parse.
- #
- # Raises a JSON::Stream::ParserError if the JSON data is malformed.
- #
- # Returns nothing.
- def <<(data)
- (@utf8 << data).each_char do |ch|
- @pos += 1
- case @state
- when :start_document
- start_value(ch)
- when :start_object
- case ch
- when QUOTE
- @state = :start_string
- @stack.push(:key)
- when RIGHT_BRACE
- end_container(:object)
- when WS
- # ignore
- else
- error("Expected object key start")
- end
- when :start_string
- case ch
- when QUOTE
- if @stack.pop == :string
- end_value(@buf)
- else # :key
- @state = :end_key
- notify(:key, @buf)
- end
- @buf = +""
- when BACKSLASH
- @state = :start_escape
- when CONTROL
- error("Control characters must be escaped")
- else
- @buf << ch
- end
- when :start_escape
- case ch
- when QUOTE, BACKSLASH, SLASH
- @buf << ch
- @state = :start_string
- when B
- @buf << "\b"
- @state = :start_string
- when F
- @buf << "\f"
- @state = :start_string
- when N
- @buf << "\n"
- @state = :start_string
- when R
- @buf << "\r"
- @state = :start_string
- when T
- @buf << "\t"
- @state = :start_string
- when U
- @state = :unicode_escape
- else
- error("Expected escaped character")
- end
- when :unicode_escape
- case ch
- when HEX
- @unicode << ch
- if @unicode.size == 4
- codepoint = @unicode.slice!(0, 4).hex
- if codepoint >= 0xD800 && codepoint <= 0xDBFF
- error("Expected low surrogate pair half") if @stack[-1].is_a?(Integer)
- @state = :start_surrogate_pair
- @stack.push(codepoint)
- elsif codepoint >= 0xDC00 && codepoint <= 0xDFFF
- high = @stack.pop
- error("Expected high surrogate pair half") unless high.is_a?(Integer)
- pair = ((high - 0xD800) * 0x400) + (codepoint - 0xDC00) + 0x10000
- @buf << pair
- @state = :start_string
- else
- @buf << codepoint
- @state = :start_string
- end
- end
- else
- error("Expected unicode escape hex digit")
- end
- when :start_surrogate_pair
- case ch
- when BACKSLASH
- @state = :start_surrogate_pair_u
- else
- error("Expected low surrogate pair half")
- end
- when :start_surrogate_pair_u
- case ch
- when U
- @state = :unicode_escape
- else
- error("Expected low surrogate pair half")
- end
- when :start_negative_number
- case ch
- when ZERO
- @state = :start_zero
- @buf << ch
- when DIGIT_1_9
- @state = :start_int
- @buf << ch
- else
- error("Expected 0-9 digit")
- end
- when :start_zero
- case ch
- when POINT
- @state = :start_float
- @buf << ch
- when EXPONENT
- @state = :start_exponent
- @buf << ch
- else
- end_value(@buf.to_i)
- @buf = +""
- @pos -= 1
- redo
- end
- when :start_float
- case ch
- when DIGIT
- @state = :in_float
- @buf << ch
- else
- error("Expected 0-9 digit")
- end
- when :in_float
- case ch
- when DIGIT
- @buf << ch
- when EXPONENT
- @state = :start_exponent
- @buf << ch
- else
- end_value(@buf.to_f)
- @buf = +""
- @pos -= 1
- redo
- end
- when :start_exponent
- case ch
- when MINUS, PLUS, DIGIT
- @state = :in_exponent
- @buf << ch
- else
- error("Expected +, -, or 0-9 digit")
- end
- when :in_exponent
- case ch
- when DIGIT
- @buf << ch
- else
- error("Expected 0-9 digit") unless @buf =~ DIGIT_END
- end_value(@buf.to_f)
- @buf = +""
- @pos -= 1
- redo
- end
- when :start_int
- case ch
- when DIGIT
- @buf << ch
- when POINT
- @state = :start_float
- @buf << ch
- when EXPONENT
- @state = :start_exponent
- @buf << ch
- else
- end_value(@buf.to_i)
- @buf = +""
- @pos -= 1
- redo
- end
- when :start_true
- keyword(TRUE_KEYWORD, true, TRUE_RE, ch)
- when :start_false
- keyword(FALSE_KEYWORD, false, FALSE_RE, ch)
- when :start_null
- keyword(NULL_KEYWORD, nil, NULL_RE, ch)
- when :end_key
- case ch
- when COLON
- @state = :key_sep
- when WS
- # ignore
- else
- error("Expected colon key separator")
- end
- when :key_sep
- start_value(ch)
- when :start_array
- case ch
- when RIGHT_BRACKET
- end_container(:array)
- when WS
- # ignore
- else
- start_value(ch)
- end
- when :end_value
- case ch
- when COMMA
- @state = :value_sep
- when RIGHT_BRACE
- end_container(:object)
- when RIGHT_BRACKET
- end_container(:array)
- when WS
- # ignore
- else
- error("Expected comma or object or array close")
- end
- when :value_sep
- if @stack[-1] == :object
- case ch
- when QUOTE
- @state = :start_string
- @stack.push(:key)
- when WS
- # ignore
- else
- error("Expected object key start")
- end
- else
- start_value(ch)
- end
- when :end_document
- error("Unexpected data") unless ch =~ WS
- end
- end
- end
-
- # Drain any remaining buffered characters into the parser to complete
- # the parsing of the document.
- #
- # This is only required when parsing a document containing a single
- # numeric value, integer or float. The parser has no other way to
- # detect when it should no longer expect additional characters with
- # which to complete the parse, so it must be signaled by a call to
- # this method.
- #
- # If you're parsing more typical object or array documents, there's no
- # need to call `finish` because the parse will complete when the final
- # closing `]` or `}` character is scanned.
- #
- # Raises a JSON::Stream::ParserError if the JSON data is malformed.
- #
- # Returns nothing.
- def finish
- # Partial multi-byte character waiting for completion bytes.
- error("Unexpected end-of-file") unless @utf8.empty?
-
- # Partial array, object, or string.
- error("Unexpected end-of-file") unless @stack.empty?
-
- case @state
- when :end_document
- # done, do nothing
- when :in_float
- end_value(@buf.to_f)
- when :in_exponent
- error("Unexpected end-of-file") unless @buf =~ DIGIT_END
- end_value(@buf.to_f)
- when :start_zero
- end_value(@buf.to_i)
- when :start_int
- end_value(@buf.to_i)
- else
- error("Unexpected end-of-file")
- end
- end
-
- private
-
- # Invoke all registered observer procs for the event type.
- #
- # type - The Symbol listener name.
- # args - The argument list to pass into the observer procs.
- #
- # Examples
- #
- # # broadcast events for {"answer": 42}
- # notify(:start_object)
- # notify(:key, "answer")
- # notify(:value, 42)
- # notify(:end_object)
- #
- # Returns nothing.
- def notify(type, *args)
- @listeners[type].each { |block| block.call(*args) }
- end
-
- # Complete an object or array container value type.
- #
- # type - The Symbol, :object or :array, of the expected type.
- #
- # Raises a JSON::Stream::ParserError if the expected container type
- # was not completed.
- #
- # Returns nothing.
- def end_container(type)
- @state = :end_value
- if @stack.pop == type
- case type
- when :object
- notify(:end_object)
- when :array
- notify(:end_array)
- end
- else
- error("Expected end of #{type}")
- end
- notify_end_document if @stack.empty?
- end
-
- # Broadcast an `end_document` event to observers after a complete JSON
- # value document (object, array, number, string, true, false, null) has
- # been parsed from the text. This is the final event sent to observers
- # and signals the parse has finished.
- #
- # Returns nothing.
- def notify_end_document
- @state = :end_document
- notify(:end_document)
- end
-
- # Parse one of the three allowed keywords: true, false, null.
- #
- # word - The String keyword ('true', 'false', 'null').
- # value - The Ruby value (true, false, nil).
- # re - The Regexp of allowed keyword characters.
- # ch - The current String character being parsed.
- #
- # Raises a JSON::Stream::ParserError if the character does not belong
- # in the expected keyword.
- #
- # Returns nothing.
- def keyword(word, value, re, ch)
- if ch =~ re
- @buf << ch
- else
- error("Expected #{word} keyword")
- end
-
- if @buf.size == word.size
- if @buf == word
- @buf = +""
- end_value(value)
- else
- error("Expected #{word} keyword")
- end
- end
- end
-
- # Process the first character of one of the seven possible JSON
- # values: object, array, string, true, false, null, number.
- #
- # ch - The current character String.
- #
- # Raises a JSON::Stream::ParserError if the character does not signal
- # the start of a value.
- #
- # Returns nothing.
- def start_value(ch)
- case ch
- when LEFT_BRACE
- notify(:start_document) if @stack.empty?
- @state = :start_object
- @stack.push(:object)
- notify(:start_object)
- when LEFT_BRACKET
- notify(:start_document) if @stack.empty?
- @state = :start_array
- @stack.push(:array)
- notify(:start_array)
- when QUOTE
- @state = :start_string
- @stack.push(:string)
- when T
- @state = :start_true
- @buf << ch
- when F
- @state = :start_false
- @buf << ch
- when N
- @state = :start_null
- @buf << ch
- when MINUS
- @state = :start_negative_number
- @buf << ch
- when ZERO
- @state = :start_zero
- @buf << ch
- when DIGIT_1_9
- @state = :start_int
- @buf << ch
- when WS
- # ignore
- else
- error("Expected value")
- end
- end
-
- # Advance the state machine and notify `value` observers that a
- # string, number or keyword (true, false, null) value was parsed.
- #
- # value - The object to broadcast to observers.
- #
- # Returns nothing.
- def end_value(value)
- @state = :end_value
- notify(:start_document) if @stack.empty?
- notify(:value, value)
- notify_end_document if @stack.empty?
- end
-
- def error(message)
- raise ParserError, "#{message}: char #{@pos}"
- end
- end
- end
-end
diff --git a/lib/completions/json_streaming_tracker.rb b/lib/completions/json_streaming_tracker.rb
deleted file mode 100644
index 26771e9f..00000000
--- a/lib/completions/json_streaming_tracker.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- class JsonStreamingTracker
- attr_reader :current_key, :current_value, :stream_consumer
-
- def initialize(stream_consumer)
- @stream_consumer = stream_consumer
- @current_key = nil
- @current_value = nil
- @tracking_array = false
- @parser = DiscourseAi::Completions::JsonStreamingParser.new
-
- @parser.key do |k|
- @current_key = k
- @current_value = nil
- end
-
- @parser.value do |value|
- if @current_key
- if @tracking_array
- @current_value << value
- stream_consumer.notify_progress(@current_key, @current_value)
- else
- stream_consumer.notify_progress(@current_key, value)
- @current_key = nil
- end
- end
- end
-
- @parser.start_array do
- @tracking_array = true
- @current_value = []
- end
-
- @parser.end_array do
- @tracking_array = false
- @current_key = nil
- @current_value = nil
- end
- end
-
- def broken?
- @broken
- end
-
- def <<(raw_json)
- # llm could send broken json
- # in that case just deal with it later
- # don't stream
- return if @broken
-
- begin
- @parser << raw_json
- rescue DiscourseAi::Completions::ParserError
- # Note: We're parsing JSON content that was itself embedded as a string inside another JSON object.
- # During the outer JSON.parse, any escaped control characters (like "\\n") are unescaped to real characters ("\n"),
- # which corrupts the inner JSON structure when passed to the parser here.
- # To handle this, we retry parsing with the string JSON-escaped again (`.dump[1..-2]`) if the first attempt fails.
- try_escape_and_parse(raw_json)
- return if @broken
- end
-
- if @parser.state == :start_string && @current_key
- buffered = @tracking_array ? [@parser.buf] : @parser.buf
- # this is is worth notifying
- stream_consumer.notify_progress(@current_key, buffered)
- end
-
- @current_key = nil if @parser.state == :end_value
- end
-
- private
-
- def try_escape_and_parse(raw_json)
- if !raw_json.is_a?(String)
- @broken = true
- return
- end
- # Escape the string as JSON and remove surrounding quotes
- escaped_json = raw_json.dump[1..-2]
- @parser << escaped_json
- rescue DiscourseAi::Completions::ParserError
- @broken = true
- end
- end
- end
-end
diff --git a/lib/completions/llm.rb b/lib/completions/llm.rb
deleted file mode 100644
index 70e84004..00000000
--- a/lib/completions/llm.rb
+++ /dev/null
@@ -1,438 +0,0 @@
-# frozen_string_literal: true
-
-# A facade that abstracts multiple LLMs behind a single interface.
-#
-# Internally, it consists of the combination of a dialect and an endpoint.
-# After receiving a prompt using our generic format, it translates it to
-# the target model and routes the completion request through the correct gateway.
-#
-# Use the .proxy method to instantiate an object.
-# It chooses the correct dialect and endpoint for the model you want to interact with.
-#
-# Tests of modules that perform LLM calls can use .with_prepared_responses to return canned responses
-# instead of relying on WebMock stubs like we did in the past.
-#
-module DiscourseAi
- module Completions
- class Llm
- UNKNOWN_MODEL = Class.new(StandardError)
-
- class << self
- def presets
- # Sam: I am not sure if it makes sense to translate model names at all
- @presets ||=
- begin
- [
- {
- id: "anthropic",
- models: [
- {
- name: "claude-3-7-sonnet-latest",
- tokens: 200_000,
- display_name: "Claude 3.7 Sonnet",
- input_cost: 3,
- cached_input_cost: 0.30,
- output_cost: 15,
- },
- {
- name: "claude-sonnet-4-0",
- tokens: 200_000,
- display_name: "Claude 4 Sonnet",
- input_cost: 3,
- cached_input_cost: 0.30,
- output_cost: 15,
- },
- {
- name: "claude-3-5-haiku-latest",
- tokens: 200_000,
- display_name: "Claude 3.5 Haiku",
- input_cost: 0.80,
- cached_input_cost: 0.08,
- output_cost: 4,
- },
- {
- name: "claude-opus-4-0",
- tokens: 200_000,
- display_name: "Claude 4 Opus",
- input_cost: 15,
- cached_input_cost: 1.50,
- output_cost: 75,
- },
- ],
- tokenizer: DiscourseAi::Tokenizer::AnthropicTokenizer,
- endpoint: "https://api.anthropic.com/v1/messages",
- provider: "anthropic",
- },
- {
- id: "google",
- models: [
- {
- name: "gemini-2.5-pro",
- tokens: 800_000,
- endpoint:
- "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro",
- display_name: "Gemini 2.5 Pro",
- input_cost: 1.25,
- oputput_cost: 10.0,
- },
- {
- name: "gemini-2.5-flash",
- tokens: 800_000,
- endpoint:
- "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash",
- display_name: "Gemini 2.5 Flash",
- input_cost: 0.30,
- output_cost: 2.50,
- },
- {
- name: "gemini-2.0-flash",
- tokens: 800_000,
- endpoint:
- "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash",
- display_name: "Gemini 2.0 Flash",
- input_cost: 0.10,
- output_cost: 0.40,
- },
- {
- name: "gemini-2.0-flash-lite",
- tokens: 800_000,
- endpoint:
- "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite",
- display_name: "Gemini 2.0 Flash Lite",
- input_cost: 0.075,
- output_cost: 0.30,
- },
- ],
- tokenizer: DiscourseAi::Tokenizer::GeminiTokenizer,
- provider: "google",
- },
- {
- id: "open_ai",
- models: [
- {
- name: "o4-mini",
- tokens: 200_000,
- display_name: "o4 Mini",
- input_cost: 1.10,
- cached_input_cost: 0.275,
- output_cost: 4.40,
- },
- {
- name: "o3",
- tokens: 200_000,
- display_name: "o3",
- input_cost: 2,
- cached_input_cost: 0.5,
- output_cost: 8,
- },
- {
- name: "gpt-4.1",
- tokens: 800_000,
- display_name: "GPT-4.1",
- input_cost: 2,
- cached_input_cost: 0.5,
- output_cost: 8,
- },
- {
- name: "gpt-4.1-mini",
- tokens: 800_000,
- display_name: "GPT-4.1 Mini",
- input_cost: 0.40,
- cached_input_cost: 0.10,
- output_cost: 1.60,
- },
- {
- name: "gpt-4.1-nano",
- tokens: 800_000,
- display_name: "GPT-4.1 Nano",
- input_cost: 0.10,
- cached_input_cost: 0.025,
- output_cost: 0.40,
- },
- ],
- tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer,
- endpoint: "https://api.openai.com/v1/chat/completions",
- provider: "open_ai",
- },
- {
- id: "samba_nova",
- models: [
- {
- name: "Meta-Llama-3.3-70B-Instruct",
- tokens: 131_072,
- display_name: "Llama 3.3 70B",
- input_cost: 0.60,
- output_cost: 1.20,
- },
- {
- name: "Meta-Llama-3.1-8B-Instruct",
- tokens: 16_384,
- display_name: "Llama 3.1 8B",
- input_cost: 0.1,
- output_cost: 0.20,
- },
- ],
- tokenizer: DiscourseAi::Tokenizer::Llama3Tokenizer,
- endpoint: "https://api.sambanova.ai/v1/chat/completions",
- provider: "samba_nova",
- },
- {
- id: "mistral",
- models: [
- {
- name: "mistral-large-latest",
- tokens: 128_000,
- display_name: "Mistral Large",
- },
- {
- name: "pixtral-large-latest",
- tokens: 128_000,
- display_name: "Pixtral Large",
- },
- ],
- tokenizer: DiscourseAi::Tokenizer::MistralTokenizer,
- endpoint: "https://api.mistral.ai/v1/chat/completions",
- provider: "mistral",
- },
- {
- id: "open_router",
- models: [
- {
- name: "x-ai/grok-3-beta",
- tokens: 131_072,
- display_name: "xAI Grok 3 Beta",
- input_cost: 3,
- output_cost: 15,
- },
- {
- name: "deepseek/deepseek-r1-0528:free",
- tokens: 163_000,
- display_name: "DeepSeek R1 0528 - free",
- },
- {
- name: "meta-llama/llama-3.3-70b-instruct",
- tokens: 131_072,
- display_name: "Llama 3.3 70B Instruct",
- input_cost: 0.05,
- output_cost: 0.25,
- },
- ],
- tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer,
- endpoint: "https://openrouter.ai/api/v1/chat/completions",
- provider: "open_router",
- },
- ]
- end
- end
-
- def provider_names
- providers = %w[
- aws_bedrock
- anthropic
- vllm
- hugging_face
- cohere
- open_ai
- google
- azure
- samba_nova
- mistral
- open_router
- ]
- if !Rails.env.production?
- providers << "fake"
- providers << "ollama"
- end
-
- providers
- end
-
- def tokenizer_names
- DiscourseAi::Tokenizer::BasicTokenizer.available_llm_tokenizers.map(&:name)
- end
-
- def valid_provider_models
- return @valid_provider_models if defined?(@valid_provider_models)
-
- valid_provider_models = []
- models_by_provider.each do |provider, models|
- valid_provider_models.concat(models.map { |model| "#{provider}:#{model}" })
- end
- @valid_provider_models = Set.new(valid_provider_models)
- end
-
- def with_prepared_responses(responses, llm: nil)
- @canned_response = DiscourseAi::Completions::Endpoints::CannedResponse.new(responses)
- @canned_llm = llm
- @prompts = []
- @prompt_options = []
-
- yield(@canned_response, llm, @prompts, @prompt_options)
- ensure
- # Don't leak prepared response if there's an exception.
- @canned_response = nil
- @canned_llm = nil
- @prompts = nil
- end
-
- def record_prompt(prompt, options)
- @prompts << prompt.dup if @prompts
- @prompt_options << options if @prompt_options
- end
-
- def prompt_options
- @prompt_options
- end
-
- def prompts
- @prompts
- end
-
- def proxy(model)
- llm_model =
- if model.is_a?(LlmModel)
- model
- else
- model_name_without_prov = model.split(":").last.to_i
-
- LlmModel.find_by(id: model_name_without_prov)
- end
-
- raise UNKNOWN_MODEL if llm_model.nil?
-
- dialect_klass = DiscourseAi::Completions::Dialects::Dialect.dialect_for(llm_model)
-
- if @canned_response
- if @canned_llm && @canned_llm != model
- raise "Invalid call LLM call, expected #{@canned_llm} but got #{model}"
- end
-
- return new(dialect_klass, nil, llm_model, gateway: @canned_response)
- end
-
- model_provider = llm_model.provider
- gateway_klass = DiscourseAi::Completions::Endpoints::Base.endpoint_for(model_provider)
-
- new(dialect_klass, gateway_klass, llm_model)
- end
- end
-
- def initialize(dialect_klass, gateway_klass, llm_model, gateway: nil)
- @dialect_klass = dialect_klass
- @gateway_klass = gateway_klass
- @gateway = gateway
- @llm_model = llm_model
- end
-
- # @param generic_prompt { DiscourseAi::Completions::Prompt } - Our generic prompt object
- # @param user { User } - User requesting the summary.
- # @param temperature { Float - Optional } - The temperature to use for the completion.
- # @param top_p { Float - Optional } - The top_p to use for the completion.
- # @param max_tokens { Integer - Optional } - The maximum number of tokens to generate.
- # @param stop_sequences { Array - Optional } - The stop sequences to use for the completion.
- # @param feature_name { String - Optional } - The feature name to use for the completion.
- # @param feature_context { Hash - Optional } - The feature context to use for the completion.
- # @param partial_tool_calls { Boolean - Optional } - If true, the completion will return partial tool calls.
- # @param output_thinking { Boolean - Optional } - If true, the completion will return the thinking output for thinking models.
- # @param response_format { Hash - Optional } - JSON schema passed to the API as the desired structured output.
- # @param [Experimental] extra_model_params { Hash - Optional } - Other params that are not available accross models. e.g. response_format JSON schema.
- #
- # @param &on_partial_blk { Block - Optional } - The passed block will get called with the LLM partial response.
- #
- # @returns String | ToolCall - Completion result.
- # if multiple tools or a tool and a message come back, the result will be an array of ToolCall / String objects.
- #
- def generate(
- prompt,
- temperature: nil,
- top_p: nil,
- max_tokens: nil,
- stop_sequences: nil,
- user:,
- feature_name: nil,
- feature_context: nil,
- partial_tool_calls: false,
- output_thinking: false,
- response_format: nil,
- extra_model_params: nil,
- cancel_manager: nil,
- &partial_read_blk
- )
- self.class.record_prompt(
- prompt,
- {
- temperature: temperature,
- top_p: top_p,
- max_tokens: max_tokens,
- stop_sequences: stop_sequences,
- user: user,
- feature_name: feature_name,
- feature_context: feature_context,
- partial_tool_calls: partial_tool_calls,
- output_thinking: output_thinking,
- response_format: response_format,
- extra_model_params: extra_model_params,
- },
- )
-
- model_params = { max_tokens: max_tokens, stop_sequences: stop_sequences }
-
- model_params[:temperature] = temperature if temperature
- model_params[:top_p] = top_p if top_p
-
- # internals expect symbolized keys, so we normalize here
- response_format =
- JSON.parse(response_format.to_json, symbolize_names: true) if response_format &&
- response_format.is_a?(Hash)
-
- model_params[:response_format] = response_format if response_format
- model_params.merge!(extra_model_params) if extra_model_params
-
- if prompt.is_a?(String)
- prompt =
- DiscourseAi::Completions::Prompt.new(
- "You are a helpful bot",
- messages: [{ type: :user, content: prompt }],
- )
- elsif prompt.is_a?(Array)
- prompt = DiscourseAi::Completions::Prompt.new(messages: prompt)
- end
-
- if !prompt.is_a?(DiscourseAi::Completions::Prompt)
- raise ArgumentError, "Prompt must be either a string, array, of Prompt object"
- end
-
- model_params.keys.each { |key| model_params.delete(key) if model_params[key].nil? }
-
- dialect = dialect_klass.new(prompt, llm_model, opts: model_params)
-
- gateway = @gateway || gateway_klass.new(llm_model)
- gateway.perform_completion!(
- dialect,
- user,
- model_params,
- feature_name: feature_name,
- feature_context: feature_context,
- partial_tool_calls: partial_tool_calls,
- output_thinking: output_thinking,
- cancel_manager: cancel_manager,
- &partial_read_blk
- )
- end
-
- def max_prompt_tokens
- llm_model.max_prompt_tokens
- end
-
- def tokenizer
- llm_model.tokenizer_class
- end
-
- attr_reader :llm_model
-
- private
-
- attr_reader :dialect_klass, :gateway_klass
- end
- end
-end
diff --git a/lib/completions/nova_message_processor.rb b/lib/completions/nova_message_processor.rb
deleted file mode 100644
index 80710c9c..00000000
--- a/lib/completions/nova_message_processor.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-# frozen_string_literal: true
-
-class DiscourseAi::Completions::NovaMessageProcessor
- class NovaToolCall
- attr_reader :name, :raw_json, :id
-
- def initialize(name, id, partial_tool_calls: false)
- @name = name
- @id = id
- @raw_json = +""
- @tool_call = DiscourseAi::Completions::ToolCall.new(id: id, name: name, parameters: {})
- @streaming_parser =
- DiscourseAi::Completions::JsonStreamingTracker.new(self) if partial_tool_calls
- end
-
- def append(json)
- @raw_json << json
- @streaming_parser << json if @streaming_parser
- end
-
- def notify_progress(key, value)
- @tool_call.partial = true
- @tool_call.parameters[key.to_sym] = value
- @has_new_data = true
- end
-
- def has_partial?
- @has_new_data
- end
-
- def partial_tool_call
- @has_new_data = false
- @tool_call
- end
-
- def to_tool_call
- parameters = JSON.parse(raw_json, symbolize_names: true)
- # we dupe to avoid poisoning the original tool call
- @tool_call = @tool_call.dup
- @tool_call.partial = false
- @tool_call.parameters = parameters
- @tool_call
- end
- end
-
- attr_reader :tool_calls, :input_tokens, :output_tokens
-
- def initialize(streaming_mode:, partial_tool_calls: false)
- @streaming_mode = streaming_mode
- @tool_calls = []
- @current_tool_call = nil
- @partial_tool_calls = partial_tool_calls
- end
-
- def to_tool_calls
- @tool_calls.map { |tool_call| tool_call.to_tool_call }
- end
-
- def process_streamed_message(parsed)
- return if !parsed
-
- result = nil
-
- if tool_start = parsed.dig(:contentBlockStart, :start, :toolUse)
- @current_tool_call = NovaToolCall.new(tool_start[:name], tool_start[:toolUseId])
- end
-
- if tool_progress = parsed.dig(:contentBlockDelta, :delta, :toolUse, :input)
- @current_tool_call.append(tool_progress)
- end
-
- result = @current_tool_call.to_tool_call if parsed[:contentBlockStop] && @current_tool_call
-
- if metadata = parsed[:metadata]
- @input_tokens = metadata.dig(:usage, :inputTokens)
- @output_tokens = metadata.dig(:usage, :outputTokens)
- end
-
- result || parsed.dig(:contentBlockDelta, :delta, :text)
- end
-
- def process_message(payload)
- result = []
- parsed = payload
- parsed = JSON.parse(payload, symbolize_names: true) if payload.is_a?(String)
-
- result << parsed.dig(:output, :message, :content, 0, :text)
-
- @input_tokens = parsed.dig(:usage, :inputTokens)
- @output_tokens = parsed.dig(:usage, :outputTokens)
-
- result
- end
-end
diff --git a/lib/completions/open_ai_message_processor.rb b/lib/completions/open_ai_message_processor.rb
deleted file mode 100644
index 979c2043..00000000
--- a/lib/completions/open_ai_message_processor.rb
+++ /dev/null
@@ -1,128 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi::Completions
- class OpenAiMessageProcessor
- attr_reader :prompt_tokens, :completion_tokens, :cached_tokens
-
- def initialize(partial_tool_calls: false)
- @tool = nil
- @tool_arguments = +""
- @prompt_tokens = nil
- @completion_tokens = nil
- @cached_tokens = nil
- @partial_tool_calls = partial_tool_calls
- end
-
- def process_message(json)
- result = []
- tool_calls = json.dig(:choices, 0, :message, :tool_calls)
-
- message = json.dig(:choices, 0, :message, :content)
- result << message if message.present?
-
- if tool_calls.present?
- tool_calls.each do |tool_call|
- id = tool_call.dig(:id)
- name = tool_call.dig(:function, :name)
- arguments = tool_call.dig(:function, :arguments)
- parameters = arguments.present? ? JSON.parse(arguments, symbolize_names: true) : {}
- result << ToolCall.new(id: id, name: name, parameters: parameters)
- end
- end
-
- update_usage(json)
-
- result
- end
-
- def process_streamed_message(json)
- rval = nil
-
- tool_calls = json.dig(:choices, 0, :delta, :tool_calls)
- content = json.dig(:choices, 0, :delta, :content)
-
- finished_tools = json.dig(:choices, 0, :finish_reason) || tool_calls == []
-
- if tool_calls.present?
- id = tool_calls.dig(0, :id)
- name = tool_calls.dig(0, :function, :name)
- arguments = tool_calls.dig(0, :function, :arguments)
-
- # TODO: multiple tool support may require index
- #index = tool_calls[0].dig(:index)
-
- if id.present? && @tool && @tool.id != id
- process_arguments
- rval = @tool
- @tool = nil
- end
-
- if id.present? && name.present?
- @tool_arguments = +""
- @tool = ToolCall.new(id: id, name: name)
- @streaming_parser = JsonStreamingTracker.new(self) if @partial_tool_calls
- end
-
- @tool_arguments << arguments.to_s
- @streaming_parser << arguments.to_s if @streaming_parser && !arguments.to_s.empty?
- rval = current_tool_progress if !rval
- elsif finished_tools && @tool
- parsed_args = JSON.parse(@tool_arguments, symbolize_names: true)
- @tool.parameters = parsed_args
- @tool.partial = false
- rval = @tool
- @tool = nil
- elsif !content.to_s.empty?
- # we don't want to strip empty content like "\n", do not use present?
- rval = content
- end
-
- update_usage(json)
-
- rval
- end
-
- def notify_progress(key, value)
- if @tool
- @tool.partial = true
- @tool.parameters[key.to_sym] = value
- @has_new_data = true
- end
- end
-
- def current_tool_progress
- if @has_new_data
- @has_new_data = false
- @tool
- else
- nil
- end
- end
-
- def finish
- rval = []
- if @tool
- process_arguments
- rval << @tool
- @tool = nil
- end
-
- rval
- end
-
- private
-
- def process_arguments
- if @tool_arguments.present?
- parsed_args = JSON.parse(@tool_arguments, symbolize_names: true)
- @tool.parameters = parsed_args
- @tool_arguments = nil
- end
- end
-
- def update_usage(json)
- @prompt_tokens ||= json.dig(:usage, :prompt_tokens)
- @completion_tokens ||= json.dig(:usage, :completion_tokens)
- @cached_tokens ||= json.dig(:usage, :prompt_tokens_details, :cached_tokens)
- end
- end
-end
diff --git a/lib/completions/open_ai_responses_message_processor.rb b/lib/completions/open_ai_responses_message_processor.rb
deleted file mode 100644
index 51381df7..00000000
--- a/lib/completions/open_ai_responses_message_processor.rb
+++ /dev/null
@@ -1,160 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi::Completions
- class OpenAiResponsesMessageProcessor
- attr_reader :prompt_tokens, :completion_tokens, :cached_tokens
-
- def initialize(partial_tool_calls: false)
- @tool = nil # currently streaming ToolCall
- @tool_arguments = +""
- @prompt_tokens = nil
- @completion_tokens = nil
- @cached_tokens = nil
- @partial_tool_calls = partial_tool_calls
- @streaming_parser = nil # JsonStreamingTracker, if used
- @has_new_data = false
- end
-
- # @param json [Hash] full JSON response from responses.create / retrieve
- # @return [Array] pieces in the order they were produced
- def process_message(json)
- result = []
-
- (json[:output] || []).each do |item|
- type = item[:type]
-
- case type
- when "function_call"
- result << build_tool_call_from_item(item)
- when "message"
- text = extract_text(item)
- result << text if text
- end
- end
-
- update_usage(json)
- result
- end
-
- # @param json [Hash] a single streamed event, already parsed from ND-JSON
- # @return [String, ToolCall, nil] only when a complete chunk is ready
- def process_streamed_message(json)
- rval = nil
- event_type = json[:type] || json["type"]
-
- case event_type
- when "response.output_text.delta"
- delta = json[:delta] || json["delta"]
- rval = delta if !delta.empty?
- when "response.output_item.added"
- item = json[:item]
- if item && item[:type] == "function_call"
- handle_tool_stream(:start, item) { |finished| rval = finished }
- end
- when "response.function_call_arguments.delta"
- delta = json[:delta]
- handle_tool_stream(:progress, delta) { |finished| rval = finished } if delta
- when "response.output_item.done"
- item = json[:item]
- if item && item[:type] == "function_call"
- handle_tool_stream(:done, item) { |finished| rval = finished }
- end
- end
-
- update_usage(json)
- rval
- end
-
- # Called by JsonStreamingTracker when partial JSON arguments are parsed
- def notify_progress(key, value)
- if @tool
- @tool.partial = true
- @tool.parameters[key.to_sym] = value
- @has_new_data = true
- end
- end
-
- def current_tool_progress
- if @has_new_data
- @has_new_data = false
- @tool
- end
- end
-
- def finish
- rval = []
- if @tool
- process_arguments
- rval << @tool
- @tool = nil
- end
- rval
- end
-
- private
-
- def extract_text(message_item)
- (message_item[:content] || message_item["content"] || [])
- .filter { |c| (c[:type] || c["type"]) == "output_text" }
- .map { |c| c[:text] || c["text"] }
- .join
- end
-
- def build_tool_call_from_item(item)
- id = item[:call_id]
- name = item[:name]
- arguments = item[:arguments] || ""
- params = arguments.empty? ? {} : JSON.parse(arguments, symbolize_names: true)
-
- ToolCall.new(id: id, name: name, parameters: params)
- end
-
- def handle_tool_stream(event_type, json)
- if event_type == :start
- start_tool_stream(json)
- elsif event_type == :progress
- @streaming_parser << json if @streaming_parser
- yield current_tool_progress
- elsif event_type == :done
- @tool_arguments << json[:arguments].to_s
- process_arguments
- finished = @tool
- @tool = nil
- yield finished
- end
- end
-
- def start_tool_stream(data)
- # important note... streaming API has both id and call_id
- # both seem to work as identifiers, api examples seem to favor call_id
- # so I am using it here
- id = data[:call_id]
- name = data[:name]
-
- @tool_arguments = +""
- @tool = ToolCall.new(id: id, name: name)
- @streaming_parser = JsonStreamingTracker.new(self) if @partial_tool_calls
- end
-
- # Parse accumulated @tool_arguments once we have a complete JSON blob
- def process_arguments
- return if @tool_arguments.to_s.empty?
- parsed = JSON.parse(@tool_arguments, symbolize_names: true)
- @tool.parameters = parsed
- @tool.partial = false
- @tool_arguments = nil
- rescue JSON::ParserError
- # leave arguments empty; caller can decide how to handle
- end
-
- def update_usage(json)
- usage = json.dig(:response, :usage)
- return if !usage
-
- cached_tokens = usage.dig(:input_tokens_details, :cached_tokens).to_i
-
- @prompt_tokens ||= usage[:input_tokens] - cached_tokens
- @completion_tokens ||= usage[:output_tokens]
- @cached_tokens ||= cached_tokens
- end
- end
-end
diff --git a/lib/completions/prompt.rb b/lib/completions/prompt.rb
deleted file mode 100644
index 0641b64f..00000000
--- a/lib/completions/prompt.rb
+++ /dev/null
@@ -1,235 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- class Prompt
- INVALID_TURN = Class.new(StandardError)
-
- attr_reader :messages, :tools, :system_message_text
- attr_accessor :topic_id, :post_id, :max_pixels, :tool_choice
-
- def self.text_only(message)
- if message[:content].is_a?(Array)
- message[:content].map { |element| element if element.is_a?(String) }.compact.join
- else
- message[:content]
- end
- end
-
- def initialize(
- system_message_text = nil,
- messages: [],
- tools: [],
- topic_id: nil,
- post_id: nil,
- max_pixels: nil,
- tool_choice: nil
- )
- raise ArgumentError, "messages must be an array" if !messages.is_a?(Array)
- raise ArgumentError, "tools must be an array" if !tools.is_a?(Array)
-
- @max_pixels = max_pixels || 1_048_576
-
- @topic_id = topic_id
- @post_id = post_id
-
- @messages = []
-
- if system_message_text
- @system_message_text = system_message_text
- @messages << { type: :system, content: @system_message_text }
- else
- @system_message_text = messages.find { |m| m[:type] == :system }&.dig(:content)
- end
-
- @messages.concat(messages)
-
- @messages.each { |message| validate_message(message) }
- @messages.each_cons(2) { |last_turn, new_turn| validate_turn(last_turn, new_turn) }
-
- self.tools = tools
- @tool_choice = tool_choice
- end
-
- def tools=(tools)
- raise ArgumentError, "tools must be an array" if !tools.is_a?(Array) && !tools.nil?
-
- @tools =
- tools.map do |tool|
- if tool.is_a?(Hash)
- ToolDefinition.from_hash(tool)
- elsif tool.is_a?(ToolDefinition)
- tool
- else
- raise ArgumentError, "tool must be a hash or a ToolDefinition was #{tool.class}"
- end
- end
- end
-
- # this new api tries to create symmetry between responses and prompts
- # this means anything we get back from the model via endpoint can be easily appended
- def push_model_response(response)
- response = [response] if !response.is_a? Array
-
- thinking, thinking_signature, redacted_thinking_signature = nil
-
- response.each do |message|
- if message.is_a?(Thinking)
- # we can safely skip partials here
- next if message.partial?
- if message.redacted
- redacted_thinking_signature = message.signature
- else
- thinking = message.message
- thinking_signature = message.signature
- end
- elsif message.is_a?(ToolCall)
- next if message.partial?
- # this is a bit surprising about the API
- # needing to add arguments is not ideal
- push(
- type: :tool_call,
- content: { arguments: message.parameters }.to_json,
- id: message.id,
- name: message.name,
- )
- elsif message.is_a?(String)
- push(type: :model, content: message)
- else
- raise ArgumentError, "response must be an array of strings, ToolCalls, or Thinkings"
- end
- end
-
- # anthropic rules are that we attach thinking to last for the response
- # it is odd, I wonder if long term we just keep thinking as a separate object
- if thinking || redacted_thinking_signature
- messages.last[:thinking] = thinking
- messages.last[:thinking_signature] = thinking_signature
- messages.last[:redacted_thinking_signature] = redacted_thinking_signature
- end
- end
-
- def push(
- type:,
- content:,
- id: nil,
- name: nil,
- thinking: nil,
- thinking_signature: nil,
- redacted_thinking_signature: nil
- )
- return if type == :system
- new_message = { type: type, content: content }
- new_message[:name] = name.to_s if name
- new_message[:id] = id.to_s if id
- new_message[:thinking] = thinking if thinking
- new_message[:thinking_signature] = thinking_signature if thinking_signature
- new_message[
- :redacted_thinking_signature
- ] = redacted_thinking_signature if redacted_thinking_signature
-
- validate_message(new_message)
- validate_turn(messages.last, new_message)
-
- messages << new_message
- end
-
- def has_tools?
- tools.present?
- end
-
- def encoded_uploads(message)
- if message[:content].is_a?(Array)
- upload_ids =
- message[:content]
- .map do |content|
- content[:upload_id] if content.is_a?(Hash) && content.key?(:upload_id)
- end
- .compact
- if !upload_ids.empty?
- return UploadEncoder.encode(upload_ids: upload_ids, max_pixels: max_pixels)
- end
- end
-
- []
- end
-
- def encode_upload(upload_id)
- UploadEncoder.encode(upload_ids: [upload_id], max_pixels: max_pixels).first
- end
-
- def content_with_encoded_uploads(content)
- return [content] unless content.is_a?(Array)
-
- content.map do |c|
- if c.is_a?(Hash) && c.key?(:upload_id)
- encode_upload(c[:upload_id])
- else
- c
- end
- end
- end
-
- def ==(other)
- return false unless other.is_a?(Prompt)
- messages == other.messages && tools == other.tools && topic_id == other.topic_id &&
- post_id == other.post_id && max_pixels == other.max_pixels &&
- tool_choice == other.tool_choice
- end
-
- def eql?(other)
- self == other
- end
-
- def hash
- [messages, tools, topic_id, post_id, max_pixels, tool_choice].hash
- end
-
- private
-
- def validate_message(message)
- valid_types = %i[system user model tool tool_call]
- if !valid_types.include?(message[:type])
- raise ArgumentError, "message type must be one of #{valid_types}"
- end
-
- valid_keys = %i[
- type
- content
- id
- name
- thinking
- thinking_signature
- redacted_thinking_signature
- ]
- if (invalid_keys = message.keys - valid_keys).any?
- raise ArgumentError, "message contains invalid keys: #{invalid_keys}"
- end
-
- if message[:content].is_a?(Array)
- message[:content].each do |content|
- if !content.is_a?(String) && !(content.is_a?(Hash) && content.keys == [:upload_id])
- raise ArgumentError, "Array message content must be a string or {upload_id: ...} "
- end
- end
- else
- if !message[:content].is_a?(String)
- raise ArgumentError, "Message content must be a string or an array"
- end
- end
- end
-
- def validate_turn(last_turn, new_turn)
- valid_types = %i[tool tool_call model user]
- raise INVALID_TURN if !valid_types.include?(new_turn[:type])
-
- if last_turn[:type] == :system && %i[tool tool_call model].include?(new_turn[:type])
- raise INVALID_TURN
- end
-
- raise INVALID_TURN if new_turn[:type] == :tool && last_turn[:type] != :tool_call
- raise INVALID_TURN if new_turn[:type] == :model && last_turn[:type] == :model
- end
- end
- end
-end
diff --git a/lib/completions/prompt_messages_builder.rb b/lib/completions/prompt_messages_builder.rb
deleted file mode 100644
index 2864846a..00000000
--- a/lib/completions/prompt_messages_builder.rb
+++ /dev/null
@@ -1,525 +0,0 @@
-# frozen_string_literal: true
-#
-module DiscourseAi
- module Completions
- class PromptMessagesBuilder
- MAX_CHAT_UPLOADS = 5
- MAX_TOPIC_UPLOADS = 5
- attr_reader :chat_context_posts
- attr_accessor :topic
-
- def self.messages_from_chat(
- message,
- channel:,
- context_post_ids:,
- max_messages:,
- include_uploads:,
- bot_user_ids:,
- instruction_message: nil
- )
- include_thread_titles = !channel.direct_message_channel? && !message.thread_id
-
- current_id = message.id
- messages = nil
-
- if !message.thread_id && channel.direct_message_channel?
- messages = [message]
- elsif !channel.direct_message_channel? && !message.thread_id
- messages =
- Chat::Message
- .joins("left join chat_threads on chat_threads.id = chat_messages.thread_id")
- .where(chat_channel_id: channel.id)
- .where(
- "chat_messages.thread_id IS NULL OR chat_threads.original_message_id = chat_messages.id",
- )
- .order(id: :desc)
- .limit(max_messages)
- .to_a
- .reverse
- end
-
- messages ||=
- ChatSDK::Thread.last_messages(
- thread_id: message.thread_id,
- guardian: Discourse.system_user.guardian,
- page_size: max_messages,
- )
-
- builder = new
-
- guardian = Guardian.new(message.user)
- if context_post_ids
- builder.set_chat_context_posts(
- context_post_ids,
- guardian,
- include_uploads: include_uploads,
- )
- end
-
- messages.each do |m|
- # restore stripped message
- m.message = instruction_message if m.id == current_id && instruction_message
-
- if bot_user_ids.include?(m.user_id)
- builder.push(type: :model, content: m.message)
- else
- upload_ids = nil
- upload_ids = m.uploads.map(&:id) if include_uploads && m.uploads.present?
- mapped_message = m.message
-
- thread_title = nil
- thread_title = m.thread&.title if include_thread_titles && m.thread_id
- mapped_message = "(#{thread_title})\n#{m.message}" if thread_title
-
- if m.uploads.present?
- mapped_message =
- "#{mapped_message} -- uploaded(#{m.uploads.map(&:short_url).join(", ")})"
- end
-
- builder.push(
- type: :user,
- content: mapped_message,
- id: m.user.username,
- upload_ids: upload_ids,
- )
- end
- end
-
- builder.to_a(
- limit: max_messages,
- style: channel.direct_message_channel? ? :chat_with_context : :chat,
- )
- end
-
- def self.messages_from_post(post, style: nil, max_posts:, bot_usernames:, include_uploads:)
- # Pay attention to the `post_number <= ?` here.
- # We want to inject the last post as context because they are translated differently.
-
- post_types = [Post.types[:regular]]
- post_types << Post.types[:whisper] if post.post_type == Post.types[:whisper]
-
- context =
- post
- .topic
- .posts
- .joins(:user)
- .joins("LEFT JOIN post_custom_prompts ON post_custom_prompts.post_id = posts.id")
- .where("post_number <= ?", post.post_number)
- .order("post_number desc")
- .where("post_type in (?)", post_types)
- .limit(max_posts)
- .pluck(
- "posts.raw",
- "users.username",
- "post_custom_prompts.custom_prompt",
- "(
- SELECT array_agg(ref.upload_id)
- FROM upload_references ref
- WHERE ref.target_type = 'Post' AND ref.target_id = posts.id
- ) as upload_ids",
- "posts.created_at",
- )
-
- builder = new
- builder.topic = post.topic
-
- context.reverse_each do |raw, username, custom_prompt, upload_ids, created_at|
- custom_prompt_translation =
- Proc.new do |message|
- # We can't keep backwards-compatibility for stored functions.
- # Tool syntax requires a tool_call_id which we don't have.
- if message[2] != "function"
- custom_context = {
- content: message[0],
- type: message[2].present? ? message[2].to_sym : :model,
- }
-
- custom_context[:id] = message[1] if custom_context[:type] != :model
- custom_context[:name] = message[3] if message[3]
-
- thinking = message[4]
- custom_context[:thinking] = thinking if thinking
- custom_context[:created_at] = created_at
-
- builder.push(**custom_context)
- end
- end
-
- if custom_prompt.present?
- custom_prompt.each(&custom_prompt_translation)
- else
- context = { content: raw, type: (bot_usernames.include?(username) ? :model : :user) }
-
- context[:id] = username if context[:type] == :user
-
- if upload_ids.present? && context[:type] == :user && include_uploads
- context[:upload_ids] = upload_ids.compact
- end
- context[:created_at] = created_at
-
- builder.push(**context)
- end
- end
-
- builder.to_a(style: style || (post.topic.private_message? ? :bot : :topic))
- end
-
- def initialize
- @raw_messages = []
- @timestamps = {}
- end
-
- def set_chat_context_posts(post_ids, guardian, include_uploads:)
- posts = []
- Post
- .where(id: post_ids)
- .order("id asc")
- .each do |post|
- next if !guardian.can_see?(post)
- posts << post
- end
- if posts.present?
- posts_context = []
- posts_context << "\nThis chat is in the context of the Discourse topic '#{posts[0].topic.title}':\n\n"
- posts_context << "{{{\n"
- posts.each do |post|
- posts_context << "url: #{post.url}\n"
- posts_context << "#{post.username}: #{post.raw}\n\n"
- if include_uploads
- post.uploads.each { |upload| posts_context << { upload_id: upload.id } }
- end
- end
- posts_context << "}}}"
- @chat_context_posts = posts_context
- end
- end
-
- def to_a(limit: nil, style: nil)
- # topic and chat array are special, they are single messages that contain all history
- return chat_array(limit: limit) if style == :chat
- return topic_array if style == :topic
-
- # the rest of the styles can include multiple messages
- result = valid_messages_array(@raw_messages)
- prepend_chat_post_context(result) if style == :chat_with_context
-
- if limit
- result[0..limit]
- else
- result
- end
- end
-
- def push(type:, content:, name: nil, upload_ids: nil, id: nil, thinking: nil, created_at: nil)
- if !%i[user model tool tool_call system].include?(type)
- raise ArgumentError, "type must be either :user, :model, :tool, :tool_call or :system"
- end
- raise ArgumentError, "upload_ids must be an array" if upload_ids && !upload_ids.is_a?(Array)
-
- content = [content, *upload_ids.map { |upload_id| { upload_id: upload_id } }] if upload_ids
- message = { type: type, content: content }
- message[:name] = name.to_s if name
- message[:id] = id.to_s if id
- if thinking
- message[:thinking] = thinking["thinking"] if thinking["thinking"]
- message[:thinking_signature] = thinking["thinking_signature"] if thinking[
- "thinking_signature"
- ]
- message[:redacted_thinking_signature] = thinking[
- "redacted_thinking_signature"
- ] if thinking["redacted_thinking_signature"]
- end
-
- @raw_messages << message
- @timestamps[message] = created_at if created_at
-
- message
- end
-
- private
-
- def valid_messages_array(messages)
- result = []
-
- # this will create a "valid" messages array
- # 1. ensures we always start with a user message
- # 2. ensures we always end with a user message
- # 3. ensures we always interleave user and model messages
- last_type = nil
- messages.each do |message|
- if message[:type] == :model && !message[:content]
- message[:content] = "Reply cancelled by user."
- end
-
- next if !last_type && message[:type] != :user
-
- if last_type == :tool_call && message[:type] != :tool
- result.pop
- last_type = result.length > 0 ? result[-1][:type] : nil
- end
-
- next if message[:type] == :tool && last_type != :tool_call
-
- if message[:type] == last_type
- # merge the message for :user message
- # replace the message for other messages
- last_message = result[-1]
-
- if message[:type] == :user
- old_name = last_message.delete(:id)
- last_message[:content] = ["#{old_name}: ", last_message[:content]].flatten if old_name
-
- new_content = message[:content]
- new_content = ["#{message[:id]}: ", new_content].flatten if message[:id]
-
- if !last_message[:content].is_a?(Array)
- last_message[:content] = [last_message[:content]]
- end
- last_message[:content].concat(["\n", new_content].flatten)
-
- compressed =
- compress_messages_buffer(last_message[:content], max_uploads: MAX_TOPIC_UPLOADS)
- last_message[:content] = compressed
- else
- last_message[:content] = message[:content]
- end
- else
- result << message
- end
-
- last_type = message[:type]
- end
-
- result
- end
-
- def prepend_chat_post_context(messages)
- return if @chat_context_posts.blank?
-
- old_content = messages[0][:content]
- old_content = [old_content] if !old_content.is_a?(Array)
-
- new_content = []
- new_content << "You are replying inside a Discourse chat.\n"
- new_content.concat(@chat_context_posts)
- new_content << "\n"
- new_content << "Your instructions are:\n"
- new_content.concat(old_content)
-
- compressed = compress_messages_buffer(new_content.flatten, max_uploads: MAX_CHAT_UPLOADS)
-
- messages[0][:content] = compressed
- end
-
- def format_user_info(user)
- info = []
- info << user_role(user)
- info << "Trust level #{user.trust_level}" if user.trust_level > 0
- info << "#{account_age(user)}"
- info << "#{user.user_stat.post_count} posts" if user.user_stat.post_count.to_i > 0
- "#{user.username} (#{user.name}): #{info.compact.join(", ")}"
- end
-
- def format_timestamp(timestamp)
- return nil unless timestamp
-
- time_diff = Time.now - timestamp
-
- if time_diff < 1.minute
- "just now"
- elsif time_diff < 1.hour
- mins = (time_diff / 1.minute).round
- "#{mins} #{mins == 1 ? "minute" : "minutes"} ago"
- elsif time_diff < 1.day
- hours = (time_diff / 1.hour).round
- "#{hours} #{hours == 1 ? "hour" : "hours"} ago"
- elsif time_diff < 7.days
- days = (time_diff / 1.day).round
- "#{days} #{days == 1 ? "day" : "days"} ago"
- elsif time_diff < 30.days
- weeks = (time_diff / 7.days).round
- "#{weeks} #{weeks == 1 ? "week" : "weeks"} ago"
- elsif time_diff < 365.days
- months = (time_diff / 30.days).round
- "#{months} #{months == 1 ? "month" : "months"} ago"
- else
- years = (time_diff / 365.days).round
- "#{years} #{years == 1 ? "year" : "years"} ago"
- end
- end
-
- def user_role(user)
- return "moderator" if user.moderator?
- return "admin" if user.admin?
- nil
- end
-
- def account_age(user)
- years = ((Time.now - user.created_at) / 1.year).round
- months = ((Time.now - user.created_at) / 1.month).round % 12
-
- output = []
- if years > 0
- output << years.to_s
- output << "year" if years == 1
- output << "years" if years > 1
- end
- if months > 0
- output << months.to_s
- output << "month" if months == 1
- output << "months" if months > 1
- end
-
- if output.empty?
- "new account"
- else
- "account age: " + output.join(" ")
- end
- end
-
- def format_topic_info(topic)
- content_array = []
-
- if topic.private_message?
- content_array << "Private message info.\n"
- else
- content_array << "Topic information:\n"
- end
-
- content_array << "- URL: #{topic.url}\n"
- content_array << "- Title: #{topic.title}\n"
- if SiteSetting.tagging_enabled
- tags = topic.tags.pluck(:name)
- tags -= DiscourseTagging.hidden_tag_names if tags.present?
- content_array << "- Tags: #{tags.join(", ")}\n" if tags.present?
- end
- if !topic.private_message?
- content_array << "- Category: #{topic.category.name}\n" if topic.category
- end
- content_array << "- Number of replies: #{topic.posts_count - 1}\n\n"
-
- content_array.join
- end
-
- def format_user_infos(usernames)
- content_array = []
-
- if usernames.present?
- users_details =
- User
- .where(username: usernames)
- .includes(:user_stat)
- .map { |user| format_user_info(user) }
- .compact
- content_array << "User information:\n"
- content_array << "- #{users_details.join("\n- ")}\n\n" if users_details.present?
- end
- content_array.join
- end
-
- def topic_array
- raw_messages = @raw_messages.dup
- content_array = []
- content_array << "You are operating in a Discourse forum.\n\n"
- content_array << format_topic_info(@topic) if @topic
-
- if raw_messages.present?
- usernames =
- raw_messages.filter { |message| message[:type] == :user }.map { |message| message[:id] }
-
- content_array << format_user_infos(usernames) if usernames.present?
- end
-
- last_user_message = raw_messages.pop
-
- if raw_messages.present?
- content_array << "Here is the conversation so far:\n"
- raw_messages.each do |message|
- content_array << "#{message[:id] || "User"}: "
- timestamp = @timestamps[message]
- content_array << "(#{format_timestamp(timestamp)}) " if timestamp
- content_array << message[:content]
- content_array << "\n\n"
- end
- end
-
- if last_user_message
- content_array << "Latest post is by #{last_user_message[:id] || "User"} who just posted:\n"
- content_array << last_user_message[:content]
- end
-
- content_array =
- compress_messages_buffer(content_array.flatten, max_uploads: MAX_TOPIC_UPLOADS)
-
- user_message = { type: :user, content: content_array }
-
- [user_message]
- end
-
- def chat_array(limit:)
- if @raw_messages.length > 1
- buffer = [
- +"You are replying inside a Discourse chat channel. Here is a summary of the conversation so far:\n{{{",
- ]
-
- @raw_messages[0..-2].each do |message|
- buffer << "\n"
-
- if message[:type] == :user
- buffer << "#{message[:id] || "User"}: "
- else
- buffer << "Bot: "
- end
-
- buffer << message[:content]
- end
-
- buffer << "\n}}}"
- buffer << "\n\n"
- buffer << "Your instructions:"
- buffer << "\n"
- end
-
- last_message = @raw_messages[-1]
- buffer << "#{last_message[:id] || "User"}: "
- buffer << last_message[:content]
-
- buffer = compress_messages_buffer(buffer.flatten, max_uploads: MAX_CHAT_UPLOADS)
-
- message = { type: :user, content: buffer }
- [message]
- end
-
- # caps uploads to maximum uploads allowed in message stream
- # and concats string elements
- def compress_messages_buffer(buffer, max_uploads:)
- compressed = []
- current_text = +""
- upload_count = 0
-
- buffer.each do |item|
- if item.is_a?(String)
- current_text << item
- elsif item.is_a?(Hash)
- compressed << current_text if current_text.present?
- compressed << item
- current_text = +""
- upload_count += 1
- end
- end
-
- compressed << current_text if current_text.present?
-
- if upload_count > max_uploads
- to_remove = upload_count - max_uploads
- removed = 0
- compressed.delete_if { |item| item.is_a?(Hash) && (removed += 1) <= to_remove }
- end
-
- compressed = compressed[0] if compressed.length == 1 && compressed[0].is_a?(String)
-
- compressed
- end
- end
- end
-end
diff --git a/lib/completions/report.rb b/lib/completions/report.rb
deleted file mode 100644
index e90ca12e..00000000
--- a/lib/completions/report.rb
+++ /dev/null
@@ -1,204 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi
- module Completions
- class Report
- UNKNOWN_FEATURE = "unknown"
- USER_LIMIT = 50
-
- attr_reader :start_date, :end_date, :base_query
-
- def initialize(start_date: 30.days.ago, end_date: Time.current)
- @start_date = start_date.beginning_of_day
- @end_date = end_date.end_of_day
- @base_query = AiApiAuditLog.where(created_at: @start_date..@end_date)
- end
-
- def total_tokens
- stats.total_tokens || 0
- end
-
- def total_cached_tokens
- stats.total_cached_tokens || 0
- end
-
- def total_request_tokens
- stats.total_request_tokens || 0
- end
-
- def total_response_tokens
- stats.total_response_tokens || 0
- end
-
- def total_requests
- stats.total_requests || 0
- end
-
- def total_spending
- total = total_input_spending + total_output_spending + total_cached_input_spending
- total.round(2)
- end
-
- def total_input_spending
- model_costs.sum { |row| row.input_cost.to_f * row.total_request_tokens.to_i / 1_000_000.0 }
- end
-
- def total_output_spending
- model_costs.sum do |row|
- row.output_cost.to_f * row.total_response_tokens.to_i / 1_000_000.0
- end
- end
-
- def total_cached_input_spending
- model_costs.sum do |row|
- row.cached_input_cost.to_f * row.total_cached_tokens.to_i / 1_000_000.0
- end
- end
-
- def stats
- @stats ||=
- base_query.select(
- "COUNT(*) as total_requests",
- "SUM(COALESCE(request_tokens + response_tokens, 0)) as total_tokens",
- "SUM(COALESCE(cached_tokens,0)) as total_cached_tokens",
- "SUM(COALESCE(request_tokens,0)) as total_request_tokens",
- "SUM(COALESCE(response_tokens,0)) as total_response_tokens",
- )[
- 0
- ]
- end
-
- def model_costs
- @model_costs ||=
- base_query
- .joins("LEFT JOIN llm_models ON llm_models.name = language_model")
- .group(
- "llm_models.name, llm_models.input_cost, llm_models.output_cost, llm_models.cached_input_cost",
- )
- .select(
- "llm_models.name",
- "llm_models.input_cost",
- "llm_models.output_cost",
- "llm_models.cached_input_cost",
- "SUM(COALESCE(request_tokens, 0)) as total_request_tokens",
- "SUM(COALESCE(response_tokens, 0)) as total_response_tokens",
- "SUM(COALESCE(cached_tokens, 0)) as total_cached_tokens",
- )
- end
-
- def guess_period(period = nil)
- period = nil if %i[day month hour].include?(period)
- period ||
- case @end_date - @start_date
- when 0..3.days
- :hour
- when 3.days..90.days
- :day
- else
- :month
- end
- end
-
- def tokens_by_period(period = nil)
- period = guess_period(period)
- base_query
- .group("DATE_TRUNC('#{period}', created_at)")
- .order("DATE_TRUNC('#{period}', created_at)")
- .select(
- "DATE_TRUNC('#{period}', created_at) as period",
- "SUM(COALESCE(request_tokens + response_tokens, 0)) as total_tokens",
- "SUM(COALESCE(cached_tokens,0)) as total_cached_tokens",
- "SUM(COALESCE(request_tokens,0)) as total_request_tokens",
- "SUM(COALESCE(response_tokens,0)) as total_response_tokens",
- )
- end
-
- def user_breakdown
- base_query
- .joins(:user)
- .joins("LEFT JOIN llm_models ON llm_models.name = language_model")
- .group(:user_id, "users.username", "users.uploaded_avatar_id")
- .order("usage_count DESC")
- .limit(USER_LIMIT)
- .select(
- "users.username",
- "users.uploaded_avatar_id",
- "COUNT(*) as usage_count",
- "SUM(COALESCE(request_tokens + response_tokens, 0)) as total_tokens",
- "SUM(COALESCE(cached_tokens,0)) as total_cached_tokens",
- "SUM(COALESCE(request_tokens,0)) as total_request_tokens",
- "SUM(COALESCE(response_tokens,0)) as total_response_tokens",
- "SUM(COALESCE(request_tokens, 0) * COALESCE(llm_models.input_cost, 0)) / 1000000.0 as input_spending",
- "SUM(COALESCE(response_tokens, 0) * COALESCE(llm_models.output_cost, 0)) / 1000000.0 as output_spending",
- "SUM(COALESCE(cached_tokens, 0) * COALESCE(llm_models.cached_input_cost, 0)) / 1000000.0 as cached_input_spending",
- )
- end
-
- def feature_breakdown
- base_query
- .joins("LEFT JOIN llm_models ON llm_models.name = language_model")
- .group(:feature_name)
- .order("usage_count DESC")
- .select(
- "case when coalesce(feature_name, '') = '' then '#{UNKNOWN_FEATURE}' else feature_name end as feature_name",
- "COUNT(*) as usage_count",
- "SUM(COALESCE(request_tokens + response_tokens, 0)) as total_tokens",
- "SUM(COALESCE(cached_tokens,0)) as total_cached_tokens",
- "SUM(COALESCE(request_tokens,0)) as total_request_tokens",
- "SUM(COALESCE(response_tokens,0)) as total_response_tokens",
- "SUM(COALESCE(request_tokens, 0) * COALESCE(llm_models.input_cost, 0)) / 1000000.0 as input_spending",
- "SUM(COALESCE(response_tokens, 0) * COALESCE(llm_models.output_cost, 0)) / 1000000.0 as output_spending",
- "SUM(COALESCE(cached_tokens, 0) * COALESCE(llm_models.cached_input_cost, 0)) / 1000000.0 as cached_input_spending",
- )
- end
-
- def model_breakdown
- base_query
- .joins("LEFT JOIN llm_models ON llm_models.name = language_model")
- .group(
- :language_model,
- "llm_models.input_cost",
- "llm_models.output_cost",
- "llm_models.cached_input_cost",
- )
- .order("usage_count DESC")
- .select(
- "language_model as llm",
- "COUNT(*) as usage_count",
- "SUM(COALESCE(request_tokens + response_tokens, 0)) as total_tokens",
- "SUM(COALESCE(cached_tokens,0)) as total_cached_tokens",
- "SUM(COALESCE(request_tokens,0)) as total_request_tokens",
- "SUM(COALESCE(response_tokens,0)) as total_response_tokens",
- "SUM(COALESCE(request_tokens, 0)) * COALESCE(llm_models.input_cost, 0) / 1000000.0 as input_spending",
- "SUM(COALESCE(response_tokens, 0)) * COALESCE(llm_models.output_cost, 0) / 1000000.0 as output_spending",
- "SUM(COALESCE(cached_tokens, 0)) * COALESCE(llm_models.cached_input_cost, 0) / 1000000.0 as cached_input_spending",
- )
- end
-
- def tokens_per_hour
- tokens_by_period(:hour)
- end
-
- def tokens_per_day
- tokens_by_period(:day)
- end
-
- def tokens_per_month
- tokens_by_period(:month)
- end
-
- def filter_by_feature(feature_name)
- if feature_name == UNKNOWN_FEATURE
- @base_query = base_query.where("coalesce(feature_name, '') = ''")
- else
- @base_query = base_query.where(feature_name: feature_name)
- end
- self
- end
-
- def filter_by_model(model_name)
- @base_query = base_query.where(language_model: model_name)
- self
- end
- end
- end
-end
diff --git a/lib/completions/structured_output.rb b/lib/completions/structured_output.rb
deleted file mode 100644
index b2e39b8f..00000000
--- a/lib/completions/structured_output.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- class StructuredOutput
- def initialize(json_schema_properties)
- @property_names = json_schema_properties.keys.map(&:to_sym)
- @property_cursors =
- json_schema_properties.reduce({}) do |m, (k, prop)|
- m[k.to_sym] = 0 if prop[:type] == "string"
- m
- end
-
- @tracked = {}
-
- @raw_response = +""
- @raw_cursor = 0
-
- @partial_json_tracker = JsonStreamingTracker.new(self)
-
- @type_map = {}
- json_schema_properties.each { |name, prop| @type_map[name.to_sym] = prop[:type].to_sym }
-
- @done = false
- end
-
- def to_s
- # we may want to also normalize the JSON here for the broken case
- @raw_response
- end
-
- attr_reader :last_chunk_buffer
-
- def <<(raw)
- raise "Cannot append to a completed StructuredOutput" if @done
- @raw_response << raw
- @partial_json_tracker << raw
- end
-
- def finish
- @done = true
- end
-
- def broken?
- @partial_json_tracker.broken?
- end
-
- def read_buffered_property(prop_name)
- if @partial_json_tracker.broken?
- if @done
- return nil if @type_map[prop_name.to_sym].nil?
- return(
- DiscourseAi::Utils::BestEffortJsonParser.extract_key(
- @raw_response,
- @type_map[prop_name.to_sym],
- prop_name,
- )
- )
- else
- return nil
- end
- end
-
- # Maybe we haven't read that part of the JSON yet.
- return nil if @tracked[prop_name].nil?
-
- # This means this property is a string and we want to return unread chunks.
- if @property_cursors[prop_name].present?
- unread = @tracked[prop_name][@property_cursors[prop_name]..]
- @property_cursors[prop_name] = @tracked[prop_name].length
- unread
- else
- # Ints and bools, and arrays are always returned as is.
- @tracked[prop_name]
- end
- end
-
- def notify_progress(key, value)
- key_sym = key.to_sym
- return if !@property_names.include?(key_sym)
-
- @tracked[key_sym] = value
- end
- end
- end
-end
diff --git a/lib/completions/thinking.rb b/lib/completions/thinking.rb
deleted file mode 100644
index eb9e6275..00000000
--- a/lib/completions/thinking.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- class Thinking
- attr_accessor :message, :signature, :redacted, :partial
-
- def initialize(message:, signature: nil, redacted: false, partial: false)
- @message = message
- @signature = signature
- @redacted = redacted
- @partial = partial
- end
-
- def partial?
- !!@partial
- end
-
- def ==(other)
- message == other.message && signature == other.signature && redacted == other.redacted &&
- partial == other.partial
- end
-
- def dup
- Thinking.new(
- message: message.dup,
- signature: signature.dup,
- redacted: redacted,
- partial: partial,
- )
- end
-
- def to_s
- "#{message} - #{signature} - #{redacted} - #{partial}"
- end
- end
- end
-end
diff --git a/lib/completions/tool_call.rb b/lib/completions/tool_call.rb
deleted file mode 100644
index c3aa047b..00000000
--- a/lib/completions/tool_call.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- class ToolCall
- attr_reader :id, :name, :parameters
- attr_accessor :partial
-
- def partial?
- !!@partial
- end
-
- def initialize(id:, name:, parameters: nil)
- @id = id
- @name = name
- self.parameters = parameters if parameters
- @parameters ||= {}
- @partial = false
- end
-
- def parameters=(parameters)
- raise ArgumentError, "parameters must be a hash" unless parameters.is_a?(Hash)
- @parameters = parameters.symbolize_keys
- end
-
- def ==(other)
- id == other.id && name == other.name && parameters == other.parameters
- end
-
- def to_s
- "#{name} - #{id} (\n#{parameters.map(&:to_s).join("\n")}\n)"
- end
-
- def dup
- call = ToolCall.new(id: id, name: name, parameters: parameters.deep_dup)
- call.partial = partial
- call
- end
- end
- end
-end
diff --git a/lib/completions/tool_definition.rb b/lib/completions/tool_definition.rb
deleted file mode 100644
index f3a49b4d..00000000
--- a/lib/completions/tool_definition.rb
+++ /dev/null
@@ -1,252 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- class ToolDefinition
- class ParameterDefinition
- ALLOWED_TYPES = %i[string boolean integer array number].freeze
- ALLOWED_KEYS = %i[name description type required enum item_type].freeze
-
- attr_reader :name, :description, :type, :required, :enum, :item_type
-
- def self.from_hash(hash)
- extra_keys = hash.keys - ALLOWED_KEYS
- if !extra_keys.empty?
- raise ArgumentError, "Unexpected keys in parameter definition: #{extra_keys}"
- end
-
- new(
- name: hash[:name],
- description: hash[:description],
- type: hash[:type],
- required: hash[:required],
- enum: hash[:enum],
- item_type: hash[:item_type],
- )
- end
-
- def initialize(name:, description:, type:, required: false, enum: nil, item_type: nil)
- raise ArgumentError, "name must be a string" if !name.is_a?(String) || name.empty?
-
- if !description.is_a?(String) || description.empty?
- raise ArgumentError, "description must be a string"
- end
-
- type_sym = type.to_sym
-
- if !ALLOWED_TYPES.include?(type_sym)
- raise ArgumentError, "type must be one of: #{ALLOWED_TYPES.join(", ")}"
- end
-
- # Validate enum if provided
- if enum
- raise ArgumentError, "enum must be an array" if !enum.is_a?(Array)
-
- # Validate enum entries match the specified type
- enum.each do |value|
- case type_sym
- when :string
- if !value.is_a?(String)
- raise ArgumentError, "enum values must be strings for type 'string'"
- end
- when :boolean
- if ![true, false].include?(value)
- raise ArgumentError, "enum values must be booleans for type 'boolean'"
- end
- when :integer
- if !value.is_a?(Integer)
- raise ArgumentError, "enum values must be integers for type 'integer'"
- end
- when :number
- if !value.is_a?(Numeric)
- raise ArgumentError, "enum values must be numbers for type 'number'"
- end
- when :array
- if !value.is_a?(Array)
- raise ArgumentError, "enum values must be arrays for type 'array'"
- end
- end
- end
- end
-
- if item_type && type_sym != :array
- raise ArgumentError, "item_type can only be specified for array type"
- end
-
- if item_type
- if !ALLOWED_TYPES.include?(item_type.to_sym)
- raise ArgumentError, "item type must be one of: #{ALLOWED_TYPES.join(", ")}"
- end
- end
-
- @name = name
- @description = description
- @type = type_sym
- @required = !!required
- @enum = enum
- @item_type = item_type ? item_type.to_sym : nil
- end
-
- def to_h
- result = { name: @name, description: @description, type: @type, required: @required }
- result[:enum] = @enum if @enum
- result[:item_type] = @item_type if @item_type
- result
- end
- end
-
- def parameters_json_schema
- properties = {}
- required = []
-
- result = { type: "object", properties: properties, required: required }
-
- parameters.each do |param|
- name = param.name
- required << name if param.required
- properties[name] = { type: param.type, description: param.description }
- properties[name][:items] = { type: param.item_type } if param.item_type
- properties[name][:enum] = param.enum if param.enum
- end
-
- result
- end
-
- attr_reader :name, :description, :parameters
-
- def self.from_hash(hash)
- allowed_keys = %i[name description parameters]
- extra_keys = hash.keys - allowed_keys
- if !extra_keys.empty?
- raise ArgumentError, "Unexpected keys in tool definition: #{extra_keys}"
- end
-
- params = hash[:parameters] || []
- parameter_objects =
- params.map do |param|
- if param.is_a?(Hash)
- ParameterDefinition.from_hash(param)
- else
- param
- end
- end
-
- new(name: hash[:name], description: hash[:description], parameters: parameter_objects)
- end
-
- def initialize(name:, description:, parameters: [])
- raise ArgumentError, "name must be a string" if !name.is_a?(String) || name.empty?
-
- if !description.is_a?(String) || description.empty?
- raise ArgumentError, "description must be a string"
- end
-
- raise ArgumentError, "parameters must be an array" if !parameters.is_a?(Array)
-
- # Check for duplicated parameter names
- param_names = parameters.map { |p| p.name }
- duplicates = param_names.select { |param_name| param_names.count(param_name) > 1 }.uniq
- if !duplicates.empty?
- raise ArgumentError, "Duplicate parameter names found: #{duplicates.join(", ")}"
- end
-
- @name = name
- @description = description
- @parameters = parameters
- end
-
- def to_h
- { name: @name, description: @description, parameters: @parameters.map(&:to_h) }
- end
-
- def coerce_parameters(params)
- result = {}
-
- return result if !params.is_a?(Hash)
-
- @parameters.each do |param_def|
- param_name = param_def.name.to_sym
-
- # Skip if parameter is not provided and not required
- next if !params.key?(param_name) && !param_def.required
-
- # Handle required but missing parameters
- if !params.key?(param_name) && param_def.required
- result[param_name] = nil
- next
- end
-
- value = params[param_name]
-
- # For array type, handle item coercion
- if param_def.type == :array
- result[param_name] = coerce_array_value(value, param_def.item_type)
- else
- result[param_name] = coerce_single_value(value, param_def.type)
- end
- end
-
- result
- end
-
- private
-
- def coerce_array_value(value, item_type)
- # Handle non-array input by attempting to parse JSON strings
- if !value.is_a?(Array)
- if value.is_a?(String)
- begin
- parsed = JSON.parse(value)
- value = parsed.is_a?(Array) ? parsed : nil
- rescue JSON::ParserError
- return nil
- end
- else
- return nil
- end
- end
-
- # No item type specified, return the array as is
- return value if !item_type
-
- # Coerce each item in the array
- value.map { |item| coerce_single_value(item, item_type) }
- end
-
- def coerce_single_value(value, type)
- result = nil
-
- case type
- when :string
- result = value.to_s
- when :integer
- if value.is_a?(Integer)
- result = value
- elsif value.is_a?(Float)
- result = value.to_i
- elsif value.is_a?(String) && value.match?(/\A-?\d+\z/)
- result = value.to_i
- end
- when :number
- if value.is_a?(Numeric)
- result = value.to_f
- elsif value.is_a?(String) && value.match?(/\A-?\d+(\.\d+)?\z/)
- result = value.to_f
- end
- when :boolean
- if value == true || value == false
- result = value
- elsif value.is_a?(String)
- if value.downcase == "true"
- result = true
- elsif value.downcase == "false"
- result = false
- end
- end
- end
-
- result
- end
- end
- end
-end
diff --git a/lib/completions/upload_encoder.rb b/lib/completions/upload_encoder.rb
deleted file mode 100644
index c2aecbaa..00000000
--- a/lib/completions/upload_encoder.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- class UploadEncoder
- def self.encode(upload_ids:, max_pixels:)
- uploads = []
- upload_ids.each do |upload_id|
- upload = Upload.find(upload_id)
- next if upload.blank?
- next if upload.width.to_i == 0 || upload.height.to_i == 0
-
- desired_extension = upload.extension
- desired_extension = "png" if upload.extension == "gif"
- desired_extension = "png" if upload.extension == "webp"
- desired_extension = "jpeg" if upload.extension == "jpg"
-
- # this keeps it very simple format wise given everyone supports png and jpg
- next if !%w[jpeg png].include?(desired_extension)
-
- original_pixels = upload.width * upload.height
-
- image = upload
-
- if original_pixels > max_pixels
- ratio = max_pixels.to_f / original_pixels
-
- new_width = (ratio * upload.width).to_i
- new_height = (ratio * upload.height).to_i
-
- image = upload.get_optimized_image(new_width, new_height, format: desired_extension)
- elsif upload.extension != desired_extension
- image =
- upload.get_optimized_image(upload.width, upload.height, format: desired_extension)
- end
-
- next if !image
-
- mime_type = MiniMime.lookup_by_filename("test.#{desired_extension}").content_type
-
- path = Discourse.store.path_for(image)
- if path.blank?
- # download is protected with a DistributedMutex
- external_copy = Discourse.store.download_safe(image)
- path = external_copy&.path
- end
-
- encoded = Base64.strict_encode64(File.read(path))
-
- uploads << { base64: encoded, mime_type: mime_type }
- end
- uploads
- end
- end
- end
-end
diff --git a/lib/completions/xml_tag_stripper.rb b/lib/completions/xml_tag_stripper.rb
deleted file mode 100644
index c1e6e641..00000000
--- a/lib/completions/xml_tag_stripper.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Completions
- class XmlTagStripper
- def initialize(tags_to_strip)
- @tags_to_strip = tags_to_strip
- @longest_tag = tags_to_strip.map(&:length).max
- @parsed = []
- end
-
- def <<(text)
- if node = @parsed[-1]
- if node[:type] == :maybe_tag
- @parsed.pop
- text = node[:content] + text
- end
- end
- @parsed.concat(parse_tags(text))
-
- @parsed, result = process_parsed(@parsed)
- result
- end
-
- def finish
- @parsed.map { |node| node[:content] }.join
- end
-
- def process_parsed(parsed)
- output = []
- buffer = []
- stack = []
-
- parsed.each do |node|
- case node[:type]
- when :text
- if stack.empty?
- output << node[:content]
- else
- buffer << node
- end
- when :open_tag
- stack << node[:name]
- buffer << node
- when :close_tag
- if stack.empty?
- output << node[:content]
- else
- if stack[0] == node[:name]
- buffer = []
- stack = []
- else
- buffer << node
- end
- end
- when :maybe_tag
- buffer << node
- end
- end
-
- result = output.join
- result = nil if result.empty?
-
- [buffer, result]
- end
-
- def parse_tags(text)
- parsed = []
-
- while true
- before, after = text.split("<", 2)
-
- parsed << { type: :text, content: before } if before && !before.empty?
-
- break if after.nil?
-
- if before.empty? && after.empty?
- parsed << { type: :maybe_tag, content: "<" }
- break
- end
-
- tag, after = after.split(">", 2)
-
- is_end_tag = tag[0] == "/"
- tag_name = tag
- tag_name = tag[1..-1] || "" if is_end_tag
-
- if !after
- found = false
- if tag_name.length <= @longest_tag
- @tags_to_strip.each do |tag_to_strip|
- if tag_to_strip.start_with?(tag_name)
- parsed << { type: :maybe_tag, content: "<" + tag }
- found = true
- break
- end
- end
- end
- parsed << { type: :text, content: "<" + tag } if !found
- break
- end
-
- raw_tag = "<" + tag + ">"
-
- if @tags_to_strip.include?(tag_name)
- parsed << {
- type: is_end_tag ? :close_tag : :open_tag,
- content: raw_tag,
- name: tag_name,
- }
- else
- parsed << { type: :text, content: raw_tag }
- end
- text = after
- end
-
- parsed
- end
- end
- end
-end
diff --git a/lib/completions/xml_tool_processor.rb b/lib/completions/xml_tool_processor.rb
deleted file mode 100644
index f20409f7..00000000
--- a/lib/completions/xml_tool_processor.rb
+++ /dev/null
@@ -1,221 +0,0 @@
-# frozen_string_literal: true
-
-# This class can be used to process a stream of text that may contain XML tool
-# calls.
-# It will return either text or ToolCall objects.
-
-module DiscourseAi
- module Completions
- class XmlToolProcessor
- def initialize(partial_tool_calls: false, tool_definitions: nil)
- @buffer = +""
- @function_buffer = +""
- @should_cancel = false
- @in_tool = false
- @partial_tool_calls = partial_tool_calls
- @partial_tools = [] if @partial_tool_calls
- @tool_definitions = tool_definitions
- end
-
- def <<(text)
- @buffer << text
- result = []
-
- if !@in_tool
- # double check if we are clearly in a tool
- search_length = text.length + 20
- search_string = @buffer[-search_length..-1] || @buffer
-
- index = search_string.rindex("")
- @in_tool = !!index
- if @in_tool
- @function_buffer = @buffer[index..-1]
- text_index = text.rindex("")
- result << text[0..text_index - 1].rstrip if text_index && text_index > 0
- end
- else
- add_to_function_buffer(text)
- end
-
- if !@in_tool
- if maybe_has_tool?(@buffer)
- split_index = text.rindex("<").to_i - 1
- if split_index >= 0
- @function_buffer = text[split_index + 1..-1] || ""
- text = text[0..split_index] || ""
- else
- add_to_function_buffer(text)
- text = ""
- end
- else
- if @function_buffer.length > 0
- result << @function_buffer
- @function_buffer = +""
- end
- end
-
- result << text if text.length > 0
- else
- @should_cancel = true if text.include?("")
- end
-
- if @should_notify_partial_tool
- @should_notify_partial_tool = false
- result << @partial_tools.last
- end
-
- result
- end
-
- def finish
- return [] if @function_buffer.blank?
-
- idx = -1
- parse_malformed_xml(@function_buffer).map do |tool|
- new_tool_call(
- id: "tool_#{idx += 1}",
- name: tool[:tool_name],
- parameters: tool[:parameters],
- )
- end
- end
-
- def should_cancel?
- @should_cancel
- end
-
- private
-
- def new_tool_call(id:, name:, parameters:)
- if tool_def = @tool_definitions&.find { |d| d.name == name }
- parameters = tool_def.coerce_parameters(parameters)
- end
- ToolCall.new(id:, name:, parameters:)
- end
-
- def add_to_function_buffer(text)
- @function_buffer << text
- detect_partial_tool_calls(@function_buffer, text) if @partial_tool_calls
- end
-
- def detect_partial_tool_calls(buffer, delta)
- parse_partial_tool_call(buffer)
- end
-
- def parse_partial_tool_call(buffer)
- match =
- buffer
- .scan(
- %r{
-
- \s*
-
- ([^<]+)
-
- \s*
-
- (.*?)
- (|\Z)
- }mx,
- )
- .to_a
- .last
-
- if match
- params = partial_parse_params(match[1])
- if params.present?
- current_tool = @partial_tools.last
- if !current_tool || current_tool.name != match[0].strip
- current_tool =
- new_tool_call(
- id: "tool_#{@partial_tools.length}",
- name: match[0].strip,
- parameters: params,
- )
- @partial_tools << current_tool
- current_tool.partial = true
- @should_notify_partial_tool = true
- end
-
- if current_tool.parameters != params
- current_tool.parameters = params
- @should_notify_partial_tool = true
- end
- end
- end
- end
-
- def partial_parse_params(params)
- params
- .scan(%r{
- <([^>]+)>
- (.*?)
- (\1>|\Z)
- }mx)
- .each_with_object({}) do |(name, value), hash|
- next if "$/, "")
- end
- end
-
- def parse_malformed_xml(input)
- input
- .scan(
- %r{
-
- \s*
-
- ([^<]+)
-
- \s*
-
- (.*?)
-
- \s*
-
- }mx,
- )
- .map do |tool_name, params|
- {
- tool_name: tool_name.strip,
- parameters:
- params
- .scan(%r{
- <([^>]+)>
- (.*?)
- \1>
- }mx)
- .each_with_object({}) do |(name, value), hash|
- hash[name.to_sym] = value.gsub(/^$/, "")
- end,
- }
- end
- end
-
- def normalize_function_ids!(function_buffer)
- function_buffer
- .css("invoke")
- .each_with_index do |invoke, index|
- if invoke.at("tool_id")
- invoke.at("tool_id").content = "tool_#{index}" if invoke.at("tool_id").content.blank?
- else
- invoke.add_child("tool_#{index}\n") if !invoke.at("tool_id")
- end
- end
- end
-
- def maybe_has_tool?(text)
- # 16 is the length of function calls
- substring = text[-16..-1] || text
- split = substring.split("<")
-
- if split.length > 1
- match = "<" + split.last
- "".start_with?(match)
- else
- substring.ends_with?("<")
- end
- end
- end
- end
-end
diff --git a/lib/configuration/embedding_defs_enumerator.rb b/lib/configuration/embedding_defs_enumerator.rb
deleted file mode 100644
index b4adac1b..00000000
--- a/lib/configuration/embedding_defs_enumerator.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require "enum_site_setting"
-
-module DiscourseAi
- module Configuration
- class EmbeddingDefsEnumerator < ::EnumSiteSetting
- def self.valid_value?(val)
- true
- end
-
- def self.values
- DB.query_hash(<<~SQL).map(&:symbolize_keys)
- SELECT display_name AS name, id AS value
- FROM embedding_definitions
- SQL
- end
- end
- end
-end
diff --git a/lib/configuration/embedding_defs_validator.rb b/lib/configuration/embedding_defs_validator.rb
deleted file mode 100644
index 6662da33..00000000
--- a/lib/configuration/embedding_defs_validator.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Configuration
- class EmbeddingDefsValidator
- def initialize(opts = {})
- @opts = opts
- end
-
- def valid_value?(val)
- val.present? || !SiteSetting.ai_embeddings_enabled
- end
-
- def error_message
- I18n.t("discourse_ai.embeddings.configuration.disable_embeddings")
- end
- end
- end
-end
diff --git a/lib/configuration/embeddings_model_validator.rb b/lib/configuration/embeddings_model_validator.rb
deleted file mode 100644
index 489f2656..00000000
--- a/lib/configuration/embeddings_model_validator.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Configuration
- class EmbeddingsModelValidator
- def initialize(opts = {})
- @opts = opts
- end
-
- def valid_value?(val)
- return true if Rails.env.test?
-
- representation =
- DiscourseAi::Embeddings::VectorRepresentations::Base.find_representation(val)
-
- return false if representation.nil?
-
- if !representation.correctly_configured?
- @representation = representation
- return false
- end
-
- if !can_generate_embeddings?(val)
- @unreachable = true
- return false
- end
-
- true
- end
-
- def error_message
- return(I18n.t("discourse_ai.embeddings.configuration.model_unreachable")) if @unreachable
-
- @representation&.configuration_hint
- end
-
- def can_generate_embeddings?(val)
- vdef = DiscourseAi::Embeddings::VectorRepresentations::Base.find_representation(val).new
- DiscourseAi::Embeddings::Vector.new(vdef).vector_from("this is a test").present?
- end
- end
- end
-end
diff --git a/lib/configuration/embeddings_module_validator.rb b/lib/configuration/embeddings_module_validator.rb
deleted file mode 100644
index cb320b30..00000000
--- a/lib/configuration/embeddings_module_validator.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Configuration
- class EmbeddingsModuleValidator
- def initialize(opts = {})
- @opts = opts
- end
-
- def valid_value?(val)
- return true if val == "f"
- return true if Rails.env.test?
-
- SiteSetting.ai_embeddings_selected_model.present?
- end
-
- def error_message
- I18n.t("discourse_ai.embeddings.configuration.choose_model")
- end
- end
- end
-end
diff --git a/lib/configuration/feature.rb b/lib/configuration/feature.rb
deleted file mode 100644
index ee62e8c3..00000000
--- a/lib/configuration/feature.rb
+++ /dev/null
@@ -1,296 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Configuration
- class Feature
- class << self
- def feature_cache
- @feature_cache ||= ::DiscourseAi::MultisiteHash.new("feature_cache")
- end
-
- def summarization_features
- feature_cache[:summarization] ||= [
- new(
- "topic_summaries",
- "ai_summarization_persona",
- DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
- DiscourseAi::Configuration::Module::SUMMARIZATION,
- ),
- new(
- "gists",
- "ai_summary_gists_persona",
- DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
- DiscourseAi::Configuration::Module::SUMMARIZATION,
- enabled_by_setting: "ai_summary_gists_enabled",
- ),
- ]
- end
-
- def search_features
- feature_cache[:search] ||= [
- new(
- "discoveries",
- "ai_bot_discover_persona",
- DiscourseAi::Configuration::Module::SEARCH_ID,
- DiscourseAi::Configuration::Module::SEARCH,
- ),
- ]
- end
-
- def discord_features
- feature_cache[:discord] ||= [
- new(
- "search",
- "ai_discord_search_persona",
- DiscourseAi::Configuration::Module::DISCORD_ID,
- DiscourseAi::Configuration::Module::DISCORD,
- ),
- ]
- end
-
- def inference_features
- feature_cache[:inference] ||= [
- new(
- "generate_concepts",
- "inferred_concepts_generate_persona",
- DiscourseAi::Configuration::Module::INFERENCE_ID,
- DiscourseAi::Configuration::Module::INFERENCE,
- ),
- new(
- "match_concepts",
- "inferred_concepts_match_persona",
- DiscourseAi::Configuration::Module::INFERENCE_ID,
- DiscourseAi::Configuration::Module::INFERENCE,
- ),
- new(
- "deduplicate_concepts",
- "inferred_concepts_deduplicate_persona",
- DiscourseAi::Configuration::Module::INFERENCE_ID,
- DiscourseAi::Configuration::Module::INFERENCE,
- ),
- ]
- end
-
- def ai_helper_features
- feature_cache[:ai_helper] ||= [
- new(
- "proofread",
- "ai_helper_proofreader_persona",
- DiscourseAi::Configuration::Module::AI_HELPER_ID,
- DiscourseAi::Configuration::Module::AI_HELPER,
- ),
- new(
- "title_suggestions",
- "ai_helper_title_suggestions_persona",
- DiscourseAi::Configuration::Module::AI_HELPER_ID,
- DiscourseAi::Configuration::Module::AI_HELPER,
- ),
- new(
- "explain",
- "ai_helper_explain_persona",
- DiscourseAi::Configuration::Module::AI_HELPER_ID,
- DiscourseAi::Configuration::Module::AI_HELPER,
- ),
- new(
- "smart_dates",
- "ai_helper_smart_dates_persona",
- DiscourseAi::Configuration::Module::AI_HELPER_ID,
- DiscourseAi::Configuration::Module::AI_HELPER,
- ),
- new(
- "markdown_tables",
- "ai_helper_markdown_tables_persona",
- DiscourseAi::Configuration::Module::AI_HELPER_ID,
- DiscourseAi::Configuration::Module::AI_HELPER,
- ),
- new(
- "translator",
- "ai_helper_translator_persona",
- DiscourseAi::Configuration::Module::AI_HELPER_ID,
- DiscourseAi::Configuration::Module::AI_HELPER,
- ),
- new(
- "custom_prompt",
- "ai_helper_custom_prompt_persona",
- DiscourseAi::Configuration::Module::AI_HELPER_ID,
- DiscourseAi::Configuration::Module::AI_HELPER,
- ),
- new(
- "image_caption",
- "ai_helper_image_caption_persona",
- DiscourseAi::Configuration::Module::AI_HELPER_ID,
- DiscourseAi::Configuration::Module::AI_HELPER,
- ),
- ]
- end
-
- def bot_features
- feature_cache[:bot] ||= [
- new(
- "bot",
- nil,
- DiscourseAi::Configuration::Module::BOT_ID,
- DiscourseAi::Configuration::Module::BOT,
- persona_ids_lookup: -> { lookup_bot_persona_ids },
- llm_models_lookup: -> { lookup_bot_llms },
- ),
- ]
- end
-
- def spam_features
- feature_cache[:spam] ||= [
- new(
- "inspect_posts",
- nil,
- DiscourseAi::Configuration::Module::SPAM_ID,
- DiscourseAi::Configuration::Module::SPAM,
- persona_ids_lookup: -> { [AiModerationSetting.spam&.ai_persona_id].compact },
- llm_models_lookup: -> { [AiModerationSetting.spam&.llm_model].compact },
- ),
- ]
- end
-
- def embeddings_features
- feature_cache[:embeddings] ||= [
- new(
- "hyde",
- "ai_embeddings_semantic_search_hyde_persona",
- DiscourseAi::Configuration::Module::EMBEDDINGS_ID,
- DiscourseAi::Configuration::Module::EMBEDDINGS,
- ),
- ]
- end
-
- def lookup_bot_persona_ids
- AiPersona
- .where(enabled: true)
- .where(
- "allow_chat_channel_mentions OR allow_chat_direct_messages OR allow_topic_mentions OR allow_personal_messages",
- )
- .pluck(:id)
- end
-
- def lookup_bot_llms
- LlmModel.where(enabled_chat_bot: true).to_a
- end
-
- def translation_features
- feature_cache[:translation] ||= [
- new(
- "locale_detector",
- "ai_translation_locale_detector_persona",
- DiscourseAi::Configuration::Module::TRANSLATION_ID,
- DiscourseAi::Configuration::Module::TRANSLATION,
- ),
- new(
- "post_raw_translator",
- "ai_translation_post_raw_translator_persona",
- DiscourseAi::Configuration::Module::TRANSLATION_ID,
- DiscourseAi::Configuration::Module::TRANSLATION,
- ),
- new(
- "topic_title_translator",
- "ai_translation_topic_title_translator_persona",
- DiscourseAi::Configuration::Module::TRANSLATION_ID,
- DiscourseAi::Configuration::Module::TRANSLATION,
- ),
- new(
- "short_text_translator",
- "ai_translation_short_text_translator_persona",
- DiscourseAi::Configuration::Module::TRANSLATION_ID,
- DiscourseAi::Configuration::Module::TRANSLATION,
- ),
- ]
- end
-
- def all
- [
- summarization_features,
- search_features,
- discord_features,
- inference_features,
- ai_helper_features,
- translation_features,
- bot_features,
- spam_features,
- embeddings_features,
- ].flatten
- end
-
- def find_features_using(persona_id:)
- all.select { |feature| feature.persona_ids.include?(persona_id) }
- end
- end
-
- def initialize(
- name,
- persona_setting,
- module_id,
- module_name,
- enabled_by_setting: "",
- persona_ids_lookup: nil,
- llm_models_lookup: nil
- )
- @name = name
- @persona_setting = persona_setting
- @module_id = module_id
- @module_name = module_name
- @enabled_by_setting = enabled_by_setting
- @persona_ids_lookup = persona_ids_lookup
- @llm_models_lookup = llm_models_lookup
- end
-
- def llm_models
- return @llm_models_lookup.call if @llm_models_lookup
- return if !persona_ids
-
- llm_models = []
- personas = AiPersona.where(id: persona_ids)
- personas.each do |persona|
- next if persona.blank?
-
- persona_klass = persona.class_instance
-
- llm_model =
- case module_name
- when DiscourseAi::Configuration::Module::SUMMARIZATION
- DiscourseAi::Summarization.find_summarization_model(persona_klass)
- when DiscourseAi::Configuration::Module::AI_HELPER
- DiscourseAi::AiHelper::Assistant.find_ai_helper_model(name, persona_klass)
- when DiscourseAi::Configuration::Module::TRANSLATION
- DiscourseAi::Translation::BaseTranslator.preferred_llm_model(persona_klass)
- when DiscourseAi::Configuration::Module::EMBEDDINGS
- DiscourseAi::Embeddings::SemanticSearch.new(nil).find_ai_hyde_model(persona_klass)
- end
-
- if llm_model.blank? && persona.default_llm_id
- llm_model = LlmModel.find_by(id: persona.default_llm_id)
- end
-
- llm_models << llm_model if llm_model
- end
-
- llm_models.compact.uniq
- end
-
- attr_reader :name, :persona_setting, :module_id, :module_name
-
- def enabled?
- @enabled_by_setting.blank? || SiteSetting.get(@enabled_by_setting)
- end
-
- def persona_ids
- if @persona_ids_lookup
- @persona_ids_lookup.call
- else
- id = SiteSetting.get(persona_setting).to_i
- if id != 0
- [id]
- else
- []
- end
- end
- end
- end
- end
-end
diff --git a/lib/configuration/llm_dependency_validator.rb b/lib/configuration/llm_dependency_validator.rb
deleted file mode 100644
index 0cf715fe..00000000
--- a/lib/configuration/llm_dependency_validator.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Configuration
- class LlmDependencyValidator
- def initialize(opts = {})
- @opts = opts
- end
-
- def valid_value?(val)
- return true if val == "f"
-
- if @opts[:name] == :ai_summarization_enabled || @opts[:name] == :ai_helper_enabled
- has_llms = LlmModel.count > 0
- @no_llms_configured = !has_llms
- has_llms
- else
- @llm_dependency_setting_name =
- DiscourseAi::Configuration::LlmValidator.new.choose_llm_setting_for(@opts[:name])
-
- SiteSetting.public_send(@llm_dependency_setting_name).present?
- end
- end
-
- def error_message
- if @llm_dependency_setting_name
- I18n.t(
- "discourse_ai.llm.configuration.set_llm_first",
- setting: @llm_dependency_setting_name,
- )
- elsif @no_llms_configured
- I18n.t("discourse_ai.llm.configuration.create_llm")
- end
- end
- end
- end
-end
diff --git a/lib/configuration/llm_enumerator.rb b/lib/configuration/llm_enumerator.rb
deleted file mode 100644
index 200fc0a2..00000000
--- a/lib/configuration/llm_enumerator.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-require "enum_site_setting"
-
-module DiscourseAi
- module Configuration
- class LlmEnumerator < ::EnumSiteSetting
- def self.global_usage
- rval = Hash.new { |h, k| h[k] = [] }
-
- if SiteSetting.ai_bot_enabled
- LlmModel
- .where("enabled_chat_bot = ?", true)
- .pluck(:id)
- .each { |llm_id| rval[llm_id] << { type: :ai_bot } }
- end
-
- # this is unconditional, so it is clear that we always signal configuration
- AiPersona
- .where("default_llm_id IS NOT NULL")
- .pluck(:default_llm_id, :name, :id)
- .each { |llm_id, name, id| rval[llm_id] << { type: :ai_persona, name: name, id: id } }
-
- if SiteSetting.ai_helper_enabled
- model_id = SiteSetting.ai_helper_model.split(":").last.to_i
- rval[model_id] << { type: :ai_helper } if model_id != 0
- end
-
- if SiteSetting.ai_helper_image_caption_model
- model_id = SiteSetting.ai_helper_image_caption_model.split(":").last.to_i
- rval[model_id] << { type: :ai_helper_image_caption } if model_id != 0
- end
-
- if SiteSetting.ai_summarization_enabled
- summarization_persona = AiPersona.find_by(id: SiteSetting.ai_summarization_persona)
- model_id = summarization_persona.default_llm_id || LlmModel.last&.id
-
- rval[model_id] << { type: :ai_summarization }
- end
-
- if SiteSetting.ai_embeddings_semantic_search_enabled
- model_id = SiteSetting.ai_embeddings_semantic_search_hyde_model.split(":").last.to_i
- rval[model_id] << { type: :ai_embeddings_semantic_search }
- end
-
- if SiteSetting.ai_spam_detection_enabled && AiModerationSetting.spam.present?
- model_id = AiModerationSetting.spam[:llm_model_id]
- rval[model_id] << { type: :ai_spam }
- end
-
- if defined?(DiscourseAutomation::Automation)
- DiscourseAutomation::Automation
- .joins(:fields)
- .where(script: %w[llm_report llm_triage])
- .where("discourse_automation_fields.name = ?", "model")
- .pluck(
- "metadata ->> 'value', discourse_automation_automations.name, discourse_automation_automations.id",
- )
- .each do |model_text, name, id|
- next if model_text.blank?
- model_id = model_text.split("custom:").last.to_i
- if model_id.present?
- if model_text =~ /custom:(\d+)/
- rval[model_id] << { type: :automation, name: name, id: id }
- end
- end
- end
- end
-
- rval
- end
-
- def self.valid_value?(val)
- true
- end
-
- # returns an array of hashes (id: , name:, vision_enabled:)
- def self.values_for_serialization(allowed_seeded_llm_ids: nil)
- builder = DB.build(<<~SQL)
- SELECT id, display_name AS name, vision_enabled
- FROM llm_models
- /*where*/
- SQL
-
- if allowed_seeded_llm_ids.is_a?(Array) && !allowed_seeded_llm_ids.empty?
- builder.where(
- "id > 0 OR id IN (:allowed_seeded_llm_ids)",
- allowed_seeded_llm_ids: allowed_seeded_llm_ids,
- )
- else
- builder.where("id > 0")
- end
-
- builder.query_hash.map(&:symbolize_keys)
- end
-
- def self.values(allowed_seeded_llms: nil)
- values = DB.query_hash(<<~SQL).map(&:symbolize_keys)
- SELECT display_name AS name, id AS value
- FROM llm_models
- SQL
-
- if allowed_seeded_llms.is_a?(Array)
- values =
- values.filter do |value_h|
- value_h[:value] > 0 || allowed_seeded_llms.include?("#{value_h[:value]}")
- end
- end
-
- values.each { |value_h| value_h[:value] = "custom:#{value_h[:value]}" }
- values
- end
- end
- end
-end
diff --git a/lib/configuration/llm_validator.rb b/lib/configuration/llm_validator.rb
deleted file mode 100644
index 36c3c63b..00000000
--- a/lib/configuration/llm_validator.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Configuration
- class InvalidSeededModelError < StandardError
- end
-
- class LlmValidator
- def initialize(opts = {})
- @opts = opts
- end
-
- def valid_value?(val)
- if val == ""
- @parent_module_name = modules_and_choose_llm_settings.invert[@opts[:name]]
-
- @parent_enabled = SiteSetting.public_send(@parent_module_name)
- return !@parent_enabled
- end
-
- allowed_seeded_model?(val)
-
- run_test(val).tap { |result| @unreachable = result }
- rescue DiscourseAi::Configuration::InvalidSeededModelError => e
- @unreachable = true
- false
- rescue StandardError => e
- raise e if Rails.env.test?
- @unreachable = true
- true
- end
-
- def run_test(val)
- DiscourseAi::Completions::Llm
- .proxy(val)
- .generate("How much is 1 + 1?", user: nil, feature_name: "llm_validator")
- .present?
- end
-
- def modules_using(llm_model)
- choose_llm_settings = modules_and_choose_llm_settings.values
-
- choose_llm_settings.select { |s| SiteSetting.public_send(s) == "custom:#{llm_model.id}" }
- end
-
- def error_message
- if @parent_enabled
- return(
- I18n.t(
- "discourse_ai.llm.configuration.disable_module_first",
- setting: @parent_module_name,
- )
- )
- end
-
- if @invalid_seeded_model
- return I18n.t("discourse_ai.llm.configuration.invalid_seeded_model")
- end
-
- return unless @unreachable
-
- I18n.t("discourse_ai.llm.configuration.model_unreachable")
- end
-
- def choose_llm_setting_for(module_enabler_setting)
- modules_and_choose_llm_settings[module_enabler_setting]
- end
-
- def modules_and_choose_llm_settings
- {
- ai_embeddings_semantic_search_enabled: :ai_embeddings_semantic_search_hyde_model,
- ai_helper_enabled: :ai_helper_model,
- ai_summarization_enabled: :ai_summarization_model,
- ai_translation_enabled: :ai_translation_model,
- }
- end
-
- def allowed_seeded_model?(val)
- id = val.split(":").last
- return true if id.to_i > 0
-
- setting = @opts[:name]
- allowed_list = SiteSetting.public_send("#{setting}_allowed_seeded_models")
-
- if allowed_list.split("|").exclude?(id)
- @invalid_seeded_model = true
- raise DiscourseAi::Configuration::InvalidSeededModelError.new
- end
- end
- end
- end
-end
diff --git a/lib/configuration/llm_vision_enumerator.rb b/lib/configuration/llm_vision_enumerator.rb
deleted file mode 100644
index c4cf1a62..00000000
--- a/lib/configuration/llm_vision_enumerator.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require "enum_site_setting"
-
-module DiscourseAi
- module Configuration
- class LlmVisionEnumerator < ::EnumSiteSetting
- def self.valid_value?(val)
- true
- end
-
- def self.values
- values = DB.query_hash(<<~SQL).map(&:symbolize_keys)
- SELECT display_name AS name, id AS value
- FROM llm_models
- WHERE vision_enabled
- SQL
-
- values.each { |value_h| value_h[:value] = "custom:#{value_h[:value]}" }
-
- values
- end
- end
- end
-end
diff --git a/lib/configuration/module.rb b/lib/configuration/module.rb
deleted file mode 100644
index 2d3a26b6..00000000
--- a/lib/configuration/module.rb
+++ /dev/null
@@ -1,128 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Configuration
- class Module
- SUMMARIZATION = "summarization"
- SEARCH = "search"
- DISCORD = "discord"
- INFERENCE = "inference"
- AI_HELPER = "ai_helper"
- TRANSLATION = "translation"
- BOT = "bot"
- SPAM = "spam"
- EMBEDDINGS = "embeddings"
-
- NAMES = [
- SUMMARIZATION,
- SEARCH,
- DISCORD,
- INFERENCE,
- AI_HELPER,
- TRANSLATION,
- BOT,
- SPAM,
- EMBEDDINGS,
- ].freeze
-
- SUMMARIZATION_ID = 1
- SEARCH_ID = 2
- DISCORD_ID = 3
- INFERENCE_ID = 4
- AI_HELPER_ID = 5
- TRANSLATION_ID = 6
- BOT_ID = 7
- SPAM_ID = 8
- EMBEDDINGS_ID = 9
-
- class << self
- def all
- [
- new(
- SUMMARIZATION_ID,
- SUMMARIZATION,
- enabled_by_setting: "ai_summarization_enabled",
- features: DiscourseAi::Configuration::Feature.summarization_features,
- ),
- new(
- SEARCH_ID,
- SEARCH,
- enabled_by_setting: "ai_bot_enabled",
- features: DiscourseAi::Configuration::Feature.search_features,
- extra_check: -> { SiteSetting.ai_bot_discover_persona.present? },
- ),
- new(
- DISCORD_ID,
- DISCORD,
- enabled_by_setting: "ai_discord_search_enabled",
- features: DiscourseAi::Configuration::Feature.discord_features,
- ),
- new(
- INFERENCE_ID,
- INFERENCE,
- enabled_by_setting: "inferred_concepts_enabled",
- features: DiscourseAi::Configuration::Feature.inference_features,
- ),
- new(
- AI_HELPER_ID,
- AI_HELPER,
- enabled_by_setting: "ai_helper_enabled",
- features: DiscourseAi::Configuration::Feature.ai_helper_features,
- ),
- new(
- TRANSLATION_ID,
- TRANSLATION,
- enabled_by_setting: "ai_translation_enabled",
- features: DiscourseAi::Configuration::Feature.translation_features,
- ),
- new(
- BOT_ID,
- BOT,
- enabled_by_setting: "ai_bot_enabled",
- features: DiscourseAi::Configuration::Feature.bot_features,
- ),
- new(
- SPAM_ID,
- SPAM,
- enabled_by_setting: "ai_spam_detection_enabled",
- features: DiscourseAi::Configuration::Feature.spam_features,
- ),
- new(
- EMBEDDINGS_ID,
- EMBEDDINGS,
- enabled_by_setting: "ai_embeddings_enabled",
- features: DiscourseAi::Configuration::Feature.embeddings_features,
- extra_check: -> { SiteSetting.ai_embeddings_semantic_search_enabled },
- ),
- ]
- end
-
- def find_by(id:)
- all.find { |m| m.id == id }
- end
- end
-
- def initialize(id, name, enabled_by_setting: nil, features: [], extra_check: nil)
- @id = id
- @name = name
- @enabled_by_setting = enabled_by_setting
- @features = features
- @extra_check = extra_check
- end
-
- attr_reader :id, :name, :enabled_by_setting, :features
-
- def enabled?
- return @extra_check.call if enabled_by_setting.blank? && @extra_check.present?
-
- enabled_setting = SiteSetting.get(enabled_by_setting)
-
- if @extra_check
- enabled_setting && @extra_check.call
- else
- enabled_setting
- end
- end
- end
- end
-end
diff --git a/lib/configuration/persona_enumerator.rb b/lib/configuration/persona_enumerator.rb
deleted file mode 100644
index c115bc50..00000000
--- a/lib/configuration/persona_enumerator.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require "enum_site_setting"
-
-module DiscourseAi
- module Configuration
- class PersonaEnumerator < ::EnumSiteSetting
- def self.valid_value?(val)
- true
- end
-
- def self.values
- AiPersona
- .all_personas(enabled_only: false)
- .map { |persona| { name: persona.name, value: persona.id } }
- end
- end
- end
-end
diff --git a/lib/configuration/spam_detection_validator.rb b/lib/configuration/spam_detection_validator.rb
deleted file mode 100644
index 6201cf3d..00000000
--- a/lib/configuration/spam_detection_validator.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Configuration
- class SpamDetectionValidator
- def initialize(opts = {})
- @opts = opts
- end
-
- def valid_value?(val)
- # only validate when enabling spam detection
- return true if val == "f" || val == "false"
- return true if AiModerationSetting.spam
-
- false
- end
-
- def error_message
- I18n.t("discourse_ai.spam_detection.configuration_missing")
- end
- end
- end
-end
diff --git a/lib/database/connection.rb b/lib/database/connection.rb
deleted file mode 100644
index f591e188..00000000
--- a/lib/database/connection.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module ::DiscourseAi
- module Database
- class Connection
- def self.db
- pg_conn = PG.connect(SiteSetting.ai_embeddings_pg_connection_string)
- MiniSql::Connection.get(pg_conn)
- end
- end
- end
-end
diff --git a/lib/discord/bot/base.rb b/lib/discord/bot/base.rb
deleted file mode 100644
index abb87ac1..00000000
--- a/lib/discord/bot/base.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Discord::Bot
- class Base
- def initialize(body)
- @interaction = JSON.parse(body, object_class: OpenStruct)
- @query = @interaction.data.options.first.value
- @token = @interaction.token
- end
-
- def handle_interaction!
- raise NotImplementedError
- end
-
- def create_reply(reply)
- api_endpoint = "https://discord.com/api/webhooks/#{SiteSetting.ai_discord_app_id}/#{@token}"
- conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
- response =
- conn.post(
- api_endpoint,
- { content: reply }.to_json,
- { "Content-Type" => "application/json" },
- )
- @reply_response = JSON.parse(response.body, symbolize_names: true)
- end
-
- def update_reply(reply)
- api_endpoint =
- "https://discord.com/api/webhooks/#{SiteSetting.ai_discord_app_id}/#{@token}/messages/@original"
- conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
- response =
- conn.patch(
- api_endpoint,
- { content: reply }.to_json,
- { "Content-Type" => "application/json" },
- )
- @last_update_response = JSON.parse(response.body, symbolize_names: true)
- end
- end
- end
-end
diff --git a/lib/discord/bot/persona_replier.rb b/lib/discord/bot/persona_replier.rb
deleted file mode 100644
index 4ce9c2fe..00000000
--- a/lib/discord/bot/persona_replier.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Discord::Bot
- class PersonaReplier < Base
- def initialize(body)
- @persona =
- AiPersona
- .all_personas(enabled_only: false)
- .find { |p| p.id == SiteSetting.ai_discord_search_persona.to_i }
- .new
- @bot =
- DiscourseAi::Personas::Bot.as(
- Discourse.system_user,
- persona: @persona,
- model: LlmModel.find(@persona.class.default_llm_id),
- )
- super(body)
- end
-
- def handle_interaction!
- last_update_sent_at = Time.now - 1
- reply = +""
- full_reply =
- @bot.reply(
- { conversation_context: [{ type: :user, content: @query }], skip_tool_details: true },
- ) do |partial, _something|
- reply << partial
- next if reply.blank?
-
- if @reply_response.nil?
- create_reply(wrap_links(reply.dup))
- elsif @last_update_response.nil?
- update_reply(wrap_links(reply.dup))
- elsif Time.now - last_update_sent_at > 1
- update_reply(wrap_links(reply.dup))
- last_update_sent_at = Time.now
- end
- end
-
- discord_reply = wrap_links(full_reply.last.first)
-
- if @reply_response.nil?
- create_reply(discord_reply)
- else
- update_reply(discord_reply)
- end
- end
-
- def wrap_links(text)
- text.gsub(%r{(?https?://[^\s]+)}, "<\\k>")
- end
- end
- end
-end
diff --git a/lib/discord/bot/search.rb b/lib/discord/bot/search.rb
deleted file mode 100644
index e33e35e7..00000000
--- a/lib/discord/bot/search.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Discord::Bot
- class Search < Base
- def initialize(body)
- @search = DiscourseAi::Personas::Tools::Search
- super(body)
- end
-
- def handle_interaction!
- results =
- @search.new(
- { search_query: @query },
- persona_options: {
- "max_results" => 10,
- },
- bot_user: nil,
- llm: nil,
- ).invoke(&Proc.new {})
-
- formatted_results = results[:rows].map.with_index { |result, index| <<~RESULT }.join("\n")
- #{index + 1}. [#{result[0]}](<#{Discourse.base_url}#{result[1]}>)
- RESULT
-
- reply = <<~REPLY
- Here are the top search results for your query:
-
- #{formatted_results}
- REPLY
-
- create_reply(reply)
- end
- end
- end
-end
diff --git a/lib/embeddings.rb b/lib/embeddings.rb
deleted file mode 100644
index 6c066935..00000000
--- a/lib/embeddings.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Embeddings
- def self.enabled?
- SiteSetting.ai_embeddings_enabled && SiteSetting.ai_embeddings_selected_model.present? &&
- EmbeddingDefinition.exists?(id: SiteSetting.ai_embeddings_selected_model)
- end
- end
-end
diff --git a/lib/embeddings/entry_point.rb b/lib/embeddings/entry_point.rb
deleted file mode 100644
index 67e2f3a3..00000000
--- a/lib/embeddings/entry_point.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Embeddings
- class EntryPoint
- def inject_into(plugin)
- # Include random topics in the suggested list *only* if there are no related topics.
- plugin.register_modifier(
- :topic_view_suggested_topics_options,
- ) do |suggested_options, topic_view|
- related_topics = topic_view.related_topics
- include_random = !related_topics || related_topics.topics.length == 0
- suggested_options.merge(include_random: include_random)
- end
-
- # Query and serialize related topics.
- plugin.add_to_class(:topic_view, :related_topics) do
- if topic.private_message? || !SiteSetting.ai_embeddings_semantic_related_topics_enabled
- return nil
- end
-
- @related_topics ||=
- ::DiscourseAi::Embeddings::SemanticTopicQuery.new(@user).list_semantic_related_topics(
- topic,
- )
- end
-
- # define_method must be used (instead of add_to_class) to make sure
- # that method still works when plugin is disabled too
- TopicView.alias_method(:categories_old, :categories)
- TopicView.define_method(:categories) do
- @categories ||= [*categories_old, *related_topics&.categories].flatten.uniq.compact
- end
-
- %i[topic_view TopicViewPosts].each do |serializer|
- plugin.add_to_serializer(
- serializer,
- :related_topics,
- include_condition: -> { SiteSetting.ai_embeddings_semantic_related_topics_enabled },
- ) do
- if object.next_page.nil? && !object.topic.private_message?
- object.related_topics.topics.map do |t|
- SuggestedTopicSerializer.new(t, scope: scope, root: false)
- end
- end
- end
- end
-
- plugin.register_html_builder("server:topic-show-after-posts-crawler") do |controller|
- ::DiscourseAi::Embeddings::SemanticRelated.related_topics_for_crawler(controller)
- end
-
- # embeddings generation.
- callback =
- Proc.new do |target|
- if DiscourseAi::Embeddings.enabled? &&
- (target.is_a?(Topic) || SiteSetting.ai_embeddings_per_post_enabled)
- Jobs.enqueue(
- :generate_embeddings,
- target_id: target.id,
- target_type: target.class.name,
- )
- end
- end
-
- plugin.on(:topic_created, &callback)
- plugin.on(:topic_edited, &callback)
- plugin.on(:post_created, &callback)
- plugin.on(:post_edited, &callback)
-
- plugin.add_api_key_scope(
- :discourse_ai,
- { search: { actions: %w[discourse_ai/embeddings/embeddings#search] } },
- )
- end
- end
- end
-end
diff --git a/lib/embeddings/schema.rb b/lib/embeddings/schema.rb
deleted file mode 100644
index a43dbbf4..00000000
--- a/lib/embeddings/schema.rb
+++ /dev/null
@@ -1,288 +0,0 @@
-# frozen_string_literal: true
-
-# We don't have AR objects for our embeddings, so this class
-# acts as an intermediary between us and the DB.
-# It lets us retrieve embeddings either symmetrically and asymmetrically,
-# and also store them.
-
-module DiscourseAi
- module Embeddings
- class Schema
- TOPICS_TABLE = "ai_topics_embeddings"
- POSTS_TABLE = "ai_posts_embeddings"
- RAG_DOCS_TABLE = "ai_document_fragments_embeddings"
-
- EMBEDDING_TARGETS = %w[topics posts document_fragments]
- EMBEDDING_TABLES = [TOPICS_TABLE, POSTS_TABLE, RAG_DOCS_TABLE]
-
- DEFAULT_HNSW_EF_SEARCH = 40
-
- MissingEmbeddingError = Class.new(StandardError)
-
- class << self
- def for(target_klass, vector_def: nil)
- vector_def =
- EmbeddingDefinition.find_by(
- id: SiteSetting.ai_embeddings_selected_model,
- ) if vector_def.nil?
- raise "Invalid embeddings selected model" if vector_def.nil?
-
- case target_klass&.name
- when "Topic"
- new(TOPICS_TABLE, "topic_id", vector_def)
- when "Post"
- new(POSTS_TABLE, "post_id", vector_def)
- when "RagDocumentFragment"
- new(RAG_DOCS_TABLE, "rag_document_fragment_id", vector_def)
- else
- raise ArgumentError, "Invalid target type for embeddings"
- end
- end
-
- def search_index_name(table, def_id)
- "ai_#{table}_embeddings_#{def_id}_1_search_bit"
- end
-
- def prepare_search_indexes(vector_def)
- EMBEDDING_TARGETS.each { |target| DB.exec <<~SQL }
- CREATE INDEX IF NOT EXISTS #{search_index_name(target, vector_def.id)} ON ai_#{target}_embeddings
- USING hnsw ((binary_quantize(embeddings)::bit(#{vector_def.dimensions})) bit_hamming_ops)
- WHERE model_id = #{vector_def.id} AND strategy_id = 1;
- SQL
- end
-
- def correctly_indexed?(vector_def)
- index_names = EMBEDDING_TARGETS.map { |t| search_index_name(t, vector_def.id) }
- indexdefs =
- DB.query_single(
- "SELECT indexdef FROM pg_indexes WHERE indexname IN (:names)",
- names: index_names,
- )
-
- return false if indexdefs.length < index_names.length
-
- indexdefs.all? do |defs|
- defs.include? "(binary_quantize(embeddings))::bit(#{vector_def.dimensions})"
- end
- end
-
- def remove_orphaned_data
- removed_defs_ids =
- DB.query_single(
- "SELECT DISTINCT(model_id) FROM #{TOPICS_TABLE} te LEFT JOIN embedding_definitions ed ON te.model_id = ed.id WHERE ed.id IS NULL",
- )
-
- EMBEDDING_TABLES.each do |t|
- DB.exec(
- "DELETE FROM #{t} WHERE model_id IN (:removed_defs)",
- removed_defs: removed_defs_ids,
- )
- end
-
- drop_index_statement =
- EMBEDDING_TARGETS
- .reduce([]) do |memo, et|
- removed_defs_ids.each do |rdi|
- memo << "DROP INDEX IF EXISTS #{search_index_name(et, rdi)};"
- end
-
- memo
- end
- .join("\n")
-
- DB.exec(drop_index_statement)
- end
- end
-
- def initialize(table, target_column, vector_def)
- @table = table
- @target_column = target_column
- @vector_def = vector_def
- end
-
- attr_reader :table, :target_column, :vector_def
-
- def find_by_embedding(embedding)
- DB.query(
- <<~SQL,
- SELECT *
- FROM #{table}
- WHERE
- model_id = :vid AND strategy_id = :vsid
- ORDER BY
- embeddings::halfvec(#{dimensions}) #{pg_function} '[:query_embedding]'::halfvec(#{dimensions})
- LIMIT 1
- SQL
- query_embedding: embedding,
- vid: vector_def.id,
- vsid: vector_def.strategy_id,
- ).first
- end
-
- def find_by_target(target)
- DB.query(
- <<~SQL,
- SELECT *
- FROM #{table}
- WHERE
- model_id = :vid AND
- strategy_id = :vsid AND
- #{target_column} = :target_id
- LIMIT 1
- SQL
- target_id: target.id,
- vid: vector_def.id,
- vsid: vector_def.strategy_id,
- ).first
- end
-
- def asymmetric_similarity_search(embedding, limit:, offset:)
- before_query = hnsw_search_workaround(limit)
-
- builder = DB.build(<<~SQL)
- WITH candidates AS (
- SELECT
- #{target_column},
- embeddings::halfvec(#{dimensions}) AS embeddings
- FROM
- #{table}
- /*join*/
- /*where*/
- ORDER BY
- binary_quantize(embeddings)::bit(#{dimensions}) <~> binary_quantize('[:query_embedding]'::halfvec(#{dimensions}))
- LIMIT :candidates_limit
- )
- SELECT
- #{target_column},
- embeddings::halfvec(#{dimensions}) #{pg_function} '[:query_embedding]'::halfvec(#{dimensions}) AS distance
- FROM
- candidates
- ORDER BY
- embeddings::halfvec(#{dimensions}) #{pg_function} '[:query_embedding]'::halfvec(#{dimensions})
- LIMIT :limit
- OFFSET :offset;
- SQL
-
- builder.where(
- "model_id = :model_id AND strategy_id = :strategy_id",
- model_id: vector_def.id,
- strategy_id: vector_def.strategy_id,
- )
-
- yield(builder) if block_given?
-
- if table == RAG_DOCS_TABLE
- # A too low limit exacerbates the the recall loss of binary quantization
- candidates_limit = [limit * 2, 100].max
- else
- candidates_limit = limit * 2
- end
-
- ActiveRecord::Base.transaction do
- DB.exec(before_query) if before_query.present?
- builder.query(
- query_embedding: embedding,
- candidates_limit: candidates_limit,
- limit: limit,
- offset: offset,
- )
- end
- rescue PG::Error => e
- Rails.logger.error("Error #{e} querying embeddings for model #{vector_def.display_name}")
- raise MissingEmbeddingError
- end
-
- def symmetric_similarity_search(record)
- limit = 200
- before_query = hnsw_search_workaround(limit)
-
- builder = DB.build(<<~SQL)
- WITH le_target AS (
- SELECT
- embeddings
- FROM
- #{table}
- WHERE
- model_id = :vid AND
- strategy_id = :vsid AND
- #{target_column} = :target_id
- LIMIT 1
- )
- SELECT #{target_column} FROM (
- SELECT
- #{target_column}, embeddings
- FROM
- #{table}
- /*join*/
- /*where*/
- ORDER BY
- binary_quantize(embeddings)::bit(#{dimensions}) <~> (
- SELECT
- binary_quantize(embeddings)::bit(#{dimensions})
- FROM
- le_target
- LIMIT 1
- )
- LIMIT #{limit}
- ) AS widenet
- ORDER BY
- embeddings::halfvec(#{dimensions}) #{pg_function} (
- SELECT
- embeddings::halfvec(#{dimensions})
- FROM
- le_target
- LIMIT 1
- )
- LIMIT #{limit / 2};
- SQL
-
- builder.where("model_id = :vid AND strategy_id = :vsid")
-
- yield(builder) if block_given?
-
- ActiveRecord::Base.transaction do
- DB.exec(before_query) if before_query.present?
- builder.query(vid: vector_def.id, vsid: vector_def.strategy_id, target_id: record.id)
- end
- rescue PG::Error => e
- Rails.logger.error("Error #{e} querying embeddings for model #{vector_def.display_name}")
- raise MissingEmbeddingError
- end
-
- def store(record, embedding, digest)
- DB.exec(
- <<~SQL,
- INSERT INTO #{table} (#{target_column}, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
- VALUES (:target_id, :model_id, :model_version, :strategy_id, :strategy_version, :digest, '[:embeddings]', :now, :now)
- ON CONFLICT (model_id, strategy_id, #{target_column})
- DO UPDATE SET
- model_version = :model_version,
- strategy_version = :strategy_version,
- digest = :digest,
- embeddings = '[:embeddings]',
- updated_at = :now
- SQL
- target_id: record.id,
- model_id: vector_def.id,
- model_version: vector_def.version,
- strategy_id: vector_def.strategy_id,
- strategy_version: vector_def.strategy_version,
- digest: digest,
- embeddings: embedding,
- now: Time.zone.now,
- )
- end
-
- private
-
- def hnsw_search_workaround(limit)
- threshold = limit * 2
-
- return "" if threshold < DEFAULT_HNSW_EF_SEARCH
- "SET LOCAL hnsw.ef_search = #{threshold};"
- end
-
- delegate :dimensions, :pg_function, to: :vector_def
- end
- end
-end
diff --git a/lib/embeddings/semantic_related.rb b/lib/embeddings/semantic_related.rb
deleted file mode 100644
index 8c0376cc..00000000
--- a/lib/embeddings/semantic_related.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Embeddings
- class SemanticRelated
- CACHE_PREFIX = "semantic-suggested-topic-"
-
- def self.clear_cache_for(topic)
- Discourse.cache.delete("semantic-suggested-topic-#{topic.id}")
- Discourse.redis.del("build-semantic-suggested-topic-#{topic.id}")
- end
-
- def related_topic_ids_for(topic)
- return [] if SiteSetting.ai_embeddings_semantic_related_topics < 1
- return [] if !DiscourseAi::Embeddings.enabled? # fail-safe in case something end up in a broken state.
-
- cache_for = results_ttl(topic)
-
- Discourse
- .cache
- .fetch(semantic_suggested_key(topic.id), expires_in: cache_for) do
- DiscourseAi::Embeddings::Schema
- .for(Topic)
- .symmetric_similarity_search(topic)
- .map(&:topic_id)
- .tap do |candidate_ids|
- # Happens when the topic doesn't have any embeddings
- # I'd rather not use Exceptions to control the flow, so this should be refactored soon
- if candidate_ids.empty? || !candidate_ids.include?(topic.id)
- raise ::DiscourseAi::Embeddings::Schema::MissingEmbeddingError,
- "No embeddings found for topic #{topic.id}"
- end
- end
- end
- rescue ::DiscourseAi::Embeddings::Schema::MissingEmbeddingError
- # avoid a flood of jobs when visiting topic
- if Discourse.redis.set(
- build_semantic_suggested_key(topic.id),
- "queued",
- ex: 15.minutes.to_i,
- nx: true,
- )
- Jobs.enqueue(:generate_embeddings, target_type: "Topic", target_id: topic.id)
- end
- []
- end
-
- def results_ttl(topic)
- case topic.created_at
- when 6.hour.ago..Time.now
- 15.minutes
- when 3.day.ago..6.hour.ago
- 1.hour
- when 15.days.ago..3.day.ago
- 12.hours
- else
- 1.week
- end
- end
-
- def self.related_topics_for_crawler(controller)
- return "" if !controller.instance_of? TopicsController
- return "" if !SiteSetting.ai_embeddings_semantic_related_topics_enabled
- return "" if SiteSetting.ai_embeddings_semantic_related_topics < 1
-
- topic_view = controller.instance_variable_get(:@topic_view)
- topic = topic_view&.topic
- return "" if !topic
-
- related_topics = SemanticTopicQuery.new(nil).list_semantic_related_topics(topic).topics
-
- return "" if related_topics.empty?
-
- ApplicationController.render(
- template: "list/related_topics",
- layout: false,
- assigns: {
- list: related_topics,
- topic: topic,
- },
- )
- end
-
- def self.clear_cache!
- Discourse
- .cache
- .keys("#{CACHE_PREFIX}*")
- .each { |key| Discourse.cache.delete(key.split(":").last) }
- end
-
- private
-
- def semantic_suggested_key(topic_id)
- "#{CACHE_PREFIX}#{topic_id}"
- end
-
- def build_semantic_suggested_key(topic_id)
- "build-#{CACHE_PREFIX}#{topic_id}"
- end
- end
- end
-end
diff --git a/lib/embeddings/semantic_search.rb b/lib/embeddings/semantic_search.rb
deleted file mode 100644
index 726f203d..00000000
--- a/lib/embeddings/semantic_search.rb
+++ /dev/null
@@ -1,249 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Embeddings
- class SemanticSearch
- def self.clear_cache_for(query)
- digest = OpenSSL::Digest::SHA1.hexdigest(query)
-
- hyde_key =
- "semantic-search-#{digest}-#{SiteSetting.ai_embeddings_semantic_search_hyde_model}"
-
- Discourse.cache.delete(hyde_key)
- Discourse.cache.delete("#{hyde_key}-#{SiteSetting.ai_embeddings_selected_model}")
- Discourse.cache.delete("-#{SiteSetting.ai_embeddings_selected_model}")
- end
-
- def initialize(guardian)
- @guardian = guardian
- end
-
- def cached_query?(query)
- digest = OpenSSL::Digest::SHA1.hexdigest(query)
- embedding_key =
- build_embedding_key(
- digest,
- SiteSetting.ai_embeddings_semantic_search_hyde_model,
- SiteSetting.ai_embeddings_selected_model,
- )
-
- Discourse.cache.read(embedding_key).present?
- end
-
- def vector
- @vector ||= DiscourseAi::Embeddings::Vector.instance
- end
-
- def hyde_embedding(search_term)
- digest = OpenSSL::Digest::SHA1.hexdigest(search_term)
- hyde_key = build_hyde_key(digest, SiteSetting.ai_embeddings_semantic_search_hyde_model)
-
- embedding_key =
- build_embedding_key(
- digest,
- SiteSetting.ai_embeddings_semantic_search_hyde_model,
- SiteSetting.ai_embeddings_selected_model,
- )
-
- hypothetical_post =
- Discourse
- .cache
- .fetch(hyde_key, expires_in: 1.week) { hypothetical_post_from(search_term) }
-
- Discourse
- .cache
- .fetch(embedding_key, expires_in: 1.week) { vector.vector_from(hypothetical_post) }
- end
-
- def embedding(search_term)
- digest = OpenSSL::Digest::SHA1.hexdigest(search_term)
- embedding_key = build_embedding_key(digest, "", SiteSetting.ai_embeddings_selected_model)
-
- Discourse.cache.fetch(embedding_key, expires_in: 1.week) { vector.vector_from(search_term) }
- end
-
- # this ensures the candidate topics are over selected
- # that way we have a much better chance of finding topics
- # if the user filtered the results or index is a bit out of date
- OVER_SELECTION_FACTOR = 4
-
- def search_for_topics(query, page = 1, hyde: true)
- max_results_per_page = 100
- limit = [Search.per_filter, max_results_per_page].min + 1
- offset = (page - 1) * limit
- search = Search.new(query, { guardian: guardian })
- search_term = search.term
-
- if search_term.blank? || search_term.length < SiteSetting.min_search_term_length
- return Post.none
- end
-
- search_embedding = nil
- search_embedding = hyde_embedding(search_term) if hyde
- search_embedding = embedding(search_term) if search_embedding.blank?
-
- over_selection_limit = limit * OVER_SELECTION_FACTOR
-
- schema = DiscourseAi::Embeddings::Schema.for(Topic)
-
- candidate_topic_ids =
- schema.asymmetric_similarity_search(
- search_embedding,
- limit: over_selection_limit,
- offset: offset,
- ).map(&:topic_id)
-
- semantic_results =
- ::Post
- .where(post_type: ::Topic.visible_post_types(guardian.user))
- .public_posts
- .where("topics.visible")
- .where(topic_id: candidate_topic_ids, post_number: 1)
- .order("array_position(ARRAY#{candidate_topic_ids}, posts.topic_id)")
- .limit(limit)
-
- query_filter_results = search.apply_filters(semantic_results)
-
- guardian.filter_allowed_categories(query_filter_results)
- end
-
- def quick_search(query)
- max_semantic_results_per_page = 100
- search = Search.new(query, { guardian: guardian })
- search_term = search.term
-
- return [] if search_term.nil? || search_term.length < SiteSetting.min_search_term_length
-
- vector = DiscourseAi::Embeddings::Vector.instance
-
- digest = OpenSSL::Digest::SHA1.hexdigest(search_term)
-
- embedding_key =
- build_embedding_key(
- digest,
- SiteSetting.ai_embeddings_semantic_search_hyde_model,
- SiteSetting.ai_embeddings_selected_model,
- )
-
- search_term_embedding =
- Discourse
- .cache
- .fetch(embedding_key, expires_in: 1.week) do
- vector.vector_from(search_term, asymetric: true)
- end
-
- candidate_post_ids =
- DiscourseAi::Embeddings::Schema
- .for(Post)
- .asymmetric_similarity_search(
- search_term_embedding,
- limit: max_semantic_results_per_page,
- offset: 0,
- )
- .map(&:post_id)
-
- semantic_results =
- ::Post
- .where(post_type: ::Topic.visible_post_types(guardian.user))
- .public_posts
- .where("topics.visible")
- .where(id: candidate_post_ids)
- .order("array_position(ARRAY#{candidate_post_ids}, posts.id)")
-
- filtered_results = search.apply_filters(semantic_results)
-
- rerank_posts_payload =
- filtered_results
- .map(&:cooked)
- .map { Nokogiri::HTML5.fragment(_1).text }
- .map { _1.truncate(2000, omission: "") }
-
- reranked_results =
- DiscourseAi::Inference::HuggingFaceTextEmbeddings.rerank(
- search_term,
- rerank_posts_payload,
- )
-
- reordered_ids = reranked_results.map { _1[:index] }.map { filtered_results[_1].id }.take(5)
-
- reranked_semantic_results =
- ::Post
- .where(post_type: ::Topic.visible_post_types(guardian.user))
- .public_posts
- .where("topics.visible")
- .where(id: reordered_ids)
- .order("array_position(ARRAY#{reordered_ids}, posts.id)")
-
- guardian.filter_allowed_categories(reranked_semantic_results)
- end
-
- def hypothetical_post_from(search_term)
- context =
- DiscourseAi::Personas::BotContext.new(
- user: @guardian.user,
- skip_tool_details: true,
- feature_name: "semantic_search_hyde",
- messages: [{ type: :user, content: search_term }],
- )
-
- bot = build_bot(@guardian.user)
- return nil if bot.nil?
-
- structured_output = nil
- raw_response = +""
- hyde_schema_key = bot.persona.response_format&.first.to_h
-
- buffer_blk =
- Proc.new do |partial, _, type|
- if type == :structured_output
- structured_output = partial
- elsif type.blank?
- # Assume response is a regular completion.
- raw_response << partial
- end
- end
-
- bot.reply(context, &buffer_blk)
-
- structured_output&.read_buffered_property(hyde_schema_key["key"]&.to_sym) || raw_response
- end
-
- # Priorities are:
- # 1. Persona's default LLM
- # 2. `ai_embeddings_semantic_search_hyde_model` setting.
- def find_ai_hyde_model(persona_klass)
- model_id =
- persona_klass.default_llm_id ||
- SiteSetting.ai_embeddings_semantic_search_hyde_model&.split(":")&.last
-
- return if model_id.blank?
-
- LlmModel.find_by(id: model_id)
- end
-
- private
-
- attr_reader :guardian
-
- def build_hyde_key(digest, hyde_model)
- "semantic-search-#{digest}-#{hyde_model}"
- end
-
- def build_embedding_key(digest, hyde_model, embedding_model)
- "#{build_hyde_key(digest, hyde_model)}-#{embedding_model}"
- end
-
- def build_bot(user)
- persona_id = SiteSetting.ai_embeddings_semantic_search_hyde_persona
-
- persona_klass = AiPersona.find_by(id: persona_id)&.class_instance
- return if persona_klass.nil?
-
- llm_model = find_ai_hyde_model(persona_klass)
- return if llm_model.nil?
-
- DiscourseAi::Personas::Bot.as(user, persona: persona_klass.new, model: llm_model)
- end
- end
- end
-end
diff --git a/lib/embeddings/semantic_topic_query.rb b/lib/embeddings/semantic_topic_query.rb
deleted file mode 100644
index db223baa..00000000
--- a/lib/embeddings/semantic_topic_query.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-class DiscourseAi::Embeddings::SemanticTopicQuery < TopicQuery
- def list_semantic_related_topics(topic)
- query_opts = {
- skip_ordering: true,
- per_page: SiteSetting.ai_embeddings_semantic_related_topics,
- unordered: true,
- }
-
- if !SiteSetting.ai_embeddings_semantic_related_include_closed_topics
- query_opts[:status] = "open"
- end
-
- list =
- create_list(:semantic_related, query_opts) do |topics|
- candidate_ids = DiscourseAi::Embeddings::SemanticRelated.new.related_topic_ids_for(topic)
-
- list = topics.where.not(id: topic.id).where(id: candidate_ids)
-
- list = DiscoursePluginRegistry.apply_modifier(:semantic_related_topics_query, list)
-
- # array_position forces the order of the topics to be preserved
- list = list.order("array_position(ARRAY#{candidate_ids}, topics.id)")
- list = remove_muted(list, @user, query_opts)
- end
- end
-end
diff --git a/lib/embeddings/strategies/truncation.rb b/lib/embeddings/strategies/truncation.rb
deleted file mode 100644
index f747c1be..00000000
--- a/lib/embeddings/strategies/truncation.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Embeddings
- module Strategies
- class Truncation
- def id
- 1
- end
-
- def version
- 1
- end
-
- def prepare_target_text(target, vdef)
- max_length = vdef.max_sequence_length - 2
-
- prepared_text =
- case target
- when Topic
- topic_truncation(target, vdef.tokenizer, max_length)
- when Post
- post_truncation(target, vdef.tokenizer, max_length)
- when RagDocumentFragment
- vdef.tokenizer.truncate(
- target.fragment,
- max_length,
- strict: SiteSetting.ai_strict_token_counting,
- )
- else
- raise ArgumentError, "Invalid target type"
- end
-
- return prepared_text if vdef.embed_prompt.blank?
-
- [vdef.embed_prompt, prepared_text].join(" ")
- end
-
- def prepare_query_text(text, vdef, asymetric: false)
- qtext = asymetric ? "#{vdef.search_prompt} #{text}" : text
- max_length = vdef.max_sequence_length - 2
-
- vdef.tokenizer.truncate(qtext, max_length, strict: SiteSetting.ai_strict_token_counting)
- end
-
- private
-
- def topic_information(topic)
- info = +""
-
- if topic&.title.present?
- info << topic.title
- info << "\n\n"
- end
- if topic&.category&.name.present?
- info << topic.category.name
- info << "\n\n"
- end
- if SiteSetting.tagging_enabled && topic&.tags.present?
- info << topic.tags.pluck(:name).join(", ")
- info << "\n\n"
- end
-
- info
- end
-
- def topic_truncation(topic, tokenizer, max_length)
- text = +topic_information(topic)
-
- if topic&.topic_embed&.embed_content_cache.present?
- text << Nokogiri::HTML5.fragment(topic.topic_embed.embed_content_cache).text
- text << "\n\n"
- end
-
- topic.posts.find_each do |post|
- text << Nokogiri::HTML5.fragment(post.cooked).text
- break if tokenizer.size(text) >= max_length #maybe keep a partial counter to speed this up?
- text << "\n\n"
- end
-
- tokenizer.truncate(text, max_length, strict: SiteSetting.ai_strict_token_counting)
- end
-
- def post_truncation(post, tokenizer, max_length)
- text = +topic_information(post.topic)
-
- if post.is_first_post? && post.topic&.topic_embed&.embed_content_cache.present?
- text << Nokogiri::HTML5.fragment(post.topic.topic_embed.embed_content_cache).text
- else
- text << Nokogiri::HTML5.fragment(post.cooked).text
- end
-
- tokenizer.truncate(text, max_length, strict: SiteSetting.ai_strict_token_counting)
- end
- end
- end
- end
-end
diff --git a/lib/embeddings/vector.rb b/lib/embeddings/vector.rb
deleted file mode 100644
index 91ff4c2e..00000000
--- a/lib/embeddings/vector.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Embeddings
- class Vector
- def self.instance
- vector_def = EmbeddingDefinition.find_by(id: SiteSetting.ai_embeddings_selected_model)
- raise "Invalid embeddings selected model" if vector_def.nil?
-
- new(vector_def)
- end
-
- def initialize(vector_definition)
- @vdef = vector_definition
- end
-
- delegate :tokenizer, to: :vdef
-
- def gen_bulk_reprensentations(relation)
- http_pool_size = 100
- pool =
- Concurrent::CachedThreadPool.new(
- min_threads: 0,
- max_threads: http_pool_size,
- idletime: 30,
- )
-
- schema = DiscourseAi::Embeddings::Schema.for(relation.first.class, vector_def: @vdef)
-
- embedding_gen = vdef.inference_client
- promised_embeddings =
- relation
- .map do |record|
- prepared_text = vdef.prepare_target_text(record)
- next if prepared_text.blank?
-
- new_digest = OpenSSL::Digest::SHA1.hexdigest(prepared_text)
- next if schema.find_by_target(record)&.digest == new_digest
-
- Concurrent::Promises
- .fulfilled_future({ target: record, text: prepared_text, digest: new_digest }, pool)
- .then_on(pool) do |w_prepared_text|
- w_prepared_text.merge(embedding: embedding_gen.perform!(w_prepared_text[:text]))
- end
- .rescue { nil } # We log the error during #perform. Skip failed embeddings.
- end
- .compact
-
- Concurrent::Promises
- .zip(*promised_embeddings)
- .value!
- .each { |e| schema.store(e[:target], e[:embedding], e[:digest]) if e.present? }
- ensure
- pool.shutdown
- pool.wait_for_termination
- end
-
- def generate_representation_from(target)
- text = vdef.prepare_target_text(target)
- return if text.blank?
-
- schema = DiscourseAi::Embeddings::Schema.for(target.class, vector_def: @vdef)
-
- new_digest = OpenSSL::Digest::SHA1.hexdigest(text)
- return if schema.find_by_target(target)&.digest == new_digest
-
- embeddings = vdef.inference_client.perform!(text)
-
- schema.store(target, embeddings, new_digest)
- end
-
- def vector_from(text, asymetric: false)
- prepared_text = vdef.prepare_query_text(text, asymetric: asymetric)
- return if prepared_text.blank?
-
- vdef.inference_client.perform!(prepared_text)
- end
-
- attr_reader :vdef
- end
- end
-end
diff --git a/lib/engine.rb b/lib/engine.rb
deleted file mode 100644
index 6c43e22e..00000000
--- a/lib/engine.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-module ::DiscourseAi
- class Engine < ::Rails::Engine
- engine_name PLUGIN_NAME
- isolate_namespace DiscourseAi
- end
-end
diff --git a/lib/guardian_extensions.rb b/lib/guardian_extensions.rb
deleted file mode 100644
index 45cb1b55..00000000
--- a/lib/guardian_extensions.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module GuardianExtensions
- def can_see_summary?(target)
- return false if !SiteSetting.ai_summarization_enabled
-
- if target.class == Topic && target.private_message?
- allowed =
- SiteSetting.ai_pm_summarization_allowed_groups_map.any? do |group_id|
- user.group_ids.include?(group_id)
- end
-
- return false if !allowed
- end
-
- has_cached_summary =
- AiSummary.exists?(target: target, summary_type: AiSummary.summary_types[:complete])
- return has_cached_summary if user.nil?
-
- has_cached_summary || can_request_summary?
- end
-
- def can_see_gists?
- return false if !SiteSetting.ai_summarization_enabled
- return false if !SiteSetting.ai_summary_gists_enabled
-
- if (ai_persona = AiPersona.find_by(id: SiteSetting.ai_summary_gists_persona)).blank?
- return false
- end
- persona_groups = ai_persona.allowed_group_ids.to_a
- return true if persona_groups.include?(Group::AUTO_GROUPS[:everyone])
- return false if anonymous?
-
- persona_groups.any? { |group_id| user.group_ids.include?(group_id) }
- end
-
- def can_request_summary?
- return false if anonymous?
-
- user_group_ids = user.group_ids
- if (ai_persona = AiPersona.find_by(id: SiteSetting.ai_summarization_persona)).blank?
- return false
- end
-
- ai_persona.allowed_group_ids.to_a.any? { |group_id| user.group_ids.include?(group_id) }
- end
-
- def can_debug_ai_bot_conversation?(target)
- return false if anonymous?
-
- return false if !can_see?(target)
-
- if !SiteSetting.discourse_ai_enabled || !SiteSetting.ai_bot_enabled ||
- !SiteSetting.ai_bot_debugging_allowed_groups_map.any?
- return false
- end
-
- user.in_any_groups?(SiteSetting.ai_bot_debugging_allowed_groups_map)
- end
-
- def can_share_ai_bot_conversation?(target)
- return false if anonymous?
-
- if !SiteSetting.discourse_ai_enabled || !SiteSetting.ai_bot_enabled ||
- !SiteSetting.ai_bot_public_sharing_allowed_groups_map.any?
- return false
- end
-
- return false if !user.in_any_groups?(SiteSetting.ai_bot_public_sharing_allowed_groups_map)
-
- # In future we may add other valid targets for AI conversation sharing,
- # for now we only support topics.
- if target.is_a?(Topic)
- return false if !target.private_message?
- return false if target.topic_allowed_groups.exists?
- allowed_user_ids = target.topic_allowed_users.pluck(:user_id)
-
- # not in PM
- return false if !allowed_user_ids.include?(user.id)
-
- # other people in PM
- return false if allowed_user_ids.any? { |id| id > 0 && id != user.id }
-
- # no bot in the PM
- bot_ids = DiscourseAi::AiBot::EntryPoint.all_bot_ids
- return false if allowed_user_ids.none? { |id| bot_ids.include?(id) }
-
- # other content in PM
- return false if target.posts.where("user_id > 0 and user_id <> ?", user.id).exists?
- else
- return false
- end
-
- true
- end
-
- def can_destroy_shared_ai_bot_conversation?(conversation)
- return false if anonymous?
-
- conversation.user_id == user.id || is_admin?
- end
- end
-end
diff --git a/lib/inference/cloudflare_workers_ai.rb b/lib/inference/cloudflare_workers_ai.rb
deleted file mode 100644
index adb9eb78..00000000
--- a/lib/inference/cloudflare_workers_ai.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module ::DiscourseAi
- module Inference
- class CloudflareWorkersAi
- def initialize(endpoint, api_token, referer = Discourse.base_url)
- @endpoint = endpoint
- @api_token = api_token
- @referer = referer
- end
-
- attr_reader :endpoint, :api_token, :referer
-
- def perform!(content)
- headers = {
- "Referer" => Discourse.base_url,
- "Content-Type" => "application/json",
- "Authorization" => "Bearer #{api_token}",
- }
-
- payload = { text: [content] }
-
- conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
- response = conn.post(endpoint, payload.to_json, headers)
-
- case response.status
- when 200
- JSON.parse(response.body, symbolize_names: true).dig(:result, :data).first
- else
- Rails.logger.warn(
- "Cloudflare Workers AI Embeddings failed with status: #{response.status} body: #{response.body}",
- )
- raise Net::HTTPBadResponse.new(response.body.to_s)
- end
- end
- end
- end
-end
diff --git a/lib/inference/discourse_reranker.rb b/lib/inference/discourse_reranker.rb
deleted file mode 100644
index 35b09799..00000000
--- a/lib/inference/discourse_reranker.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module ::DiscourseAi
- module Inference
- class DiscourseReranker
- def self.perform!(endpoint, model, content, candidates, api_key)
- headers = { "Referer" => Discourse.base_url, "Content-Type" => "application/json" }
-
- headers["X-API-KEY"] = api_key if api_key.present?
-
- conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
- response =
- conn.post(
- endpoint,
- { model: model, content: content, candidates: candidates }.to_json,
- headers,
- )
-
- raise Net::HTTPBadResponse unless response.status == 200
-
- JSON.parse(response.body, symbolize_names: true)
- end
- end
- end
-end
diff --git a/lib/inference/gemini_embeddings.rb b/lib/inference/gemini_embeddings.rb
deleted file mode 100644
index b95cf03d..00000000
--- a/lib/inference/gemini_embeddings.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module ::DiscourseAi
- module Inference
- class GeminiEmbeddings
- def initialize(embedding_url, api_key, referer = Discourse.base_url)
- @api_key = api_key
- @embedding_url = embedding_url
- @referer = referer
- end
-
- attr_reader :embedding_url, :api_key, :referer
-
- def perform!(content)
- headers = { "Referer" => referer, "Content-Type" => "application/json" }
- url = "#{embedding_url}\?key\=#{api_key}"
- body = { content: { parts: [{ text: content }] } }
-
- conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
- response = conn.post(url, body.to_json, headers)
-
- case response.status
- when 200
- JSON.parse(response.body, symbolize_names: true).dig(:embedding, :values)
- else
- Rails.logger.warn(
- "Google Gemini Embeddings failed with status: #{response.status} body: #{response.body}",
- )
- raise Net::HTTPBadResponse.new(response.body.to_s)
- end
- end
- end
- end
-end
diff --git a/lib/inference/hugging_face_text_embeddings.rb b/lib/inference/hugging_face_text_embeddings.rb
deleted file mode 100644
index 67a964f8..00000000
--- a/lib/inference/hugging_face_text_embeddings.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-module ::DiscourseAi
- module Inference
- class HuggingFaceTextEmbeddings
- def initialize(endpoint, key, referer = Discourse.base_url)
- @endpoint = endpoint
- @key = key
- @referer = referer
- end
-
- attr_reader :endpoint, :key, :referer
-
- class << self
- def reranker_configured?
- SiteSetting.ai_hugging_face_tei_reranker_endpoint.present? ||
- SiteSetting.ai_hugging_face_tei_reranker_endpoint_srv.present?
- end
-
- def rerank(content, candidates)
- headers = { "Referer" => Discourse.base_url, "Content-Type" => "application/json" }
- body = { query: content, texts: candidates, truncate: true }.to_json
-
- if SiteSetting.ai_hugging_face_tei_reranker_endpoint_srv.present?
- service =
- DiscourseAi::Utils::DnsSrv.lookup(
- SiteSetting.ai_hugging_face_tei_reranker_endpoint_srv,
- )
- api_endpoint = "https://#{service.target}:#{service.port}"
- else
- api_endpoint = SiteSetting.ai_hugging_face_tei_reranker_endpoint
- end
-
- if SiteSetting.ai_hugging_face_tei_reranker_api_key.present?
- headers["X-API-KEY"] = SiteSetting.ai_hugging_face_tei_reranker_api_key
- headers["Authorization"] = "Bearer #{SiteSetting.ai_hugging_face_tei_reranker_api_key}"
- end
-
- conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
- response = conn.post("#{api_endpoint}/rerank", body, headers)
-
- if response.status != 200
- raise Net::HTTPBadResponse.new("Status: #{response.status}\n\n#{response.body}")
- end
-
- JSON.parse(response.body, symbolize_names: true)
- end
- end
-
- def classify_by_sentiment!(content)
- response = do_request!(content)
-
- JSON.parse(response.body, symbolize_names: true)
- end
-
- def perform!(content)
- response = do_request!(content)
-
- JSON.parse(response.body, symbolize_names: true).first
- end
-
- private
-
- def do_request!(content)
- headers = { "Referer" => referer, "Content-Type" => "application/json" }
- body = { inputs: content, truncate: true }.to_json
-
- if key.present?
- headers["X-API-KEY"] = key
- headers["Authorization"] = "Bearer #{key}"
- end
-
- conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
- response = conn.post(endpoint, body, headers)
-
- raise Net::HTTPBadResponse.new(response.body.to_s) if ![200].include?(response.status)
-
- response
- end
- end
- end
-end
diff --git a/lib/inference/open_ai_embeddings.rb b/lib/inference/open_ai_embeddings.rb
deleted file mode 100644
index ec1845d9..00000000
--- a/lib/inference/open_ai_embeddings.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module ::DiscourseAi
- module Inference
- class OpenAiEmbeddings
- def initialize(endpoint, api_key, model, dimensions)
- @endpoint = endpoint
- @api_key = api_key
- @model = model
- @dimensions = dimensions
- end
-
- attr_reader :endpoint, :api_key, :model, :dimensions
-
- def perform!(content)
- headers = { "Content-Type" => "application/json" }
-
- if endpoint.include?("azure")
- headers["api-key"] = api_key
- else
- headers["Authorization"] = "Bearer #{api_key}"
- end
-
- payload = { model: model, input: content }
- payload[:dimensions] = dimensions if dimensions.present?
-
- conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
- response = conn.post(endpoint, payload.to_json, headers)
-
- case response.status
- when 200
- JSON.parse(response.body, symbolize_names: true).dig(:data, 0, :embedding)
- else
- Rails.logger.warn(
- "OpenAI Embeddings failed with status: #{response.status} body: #{response.body}",
- )
- raise Net::HTTPBadResponse.new(response.body.to_s)
- end
- end
- end
- end
-end
diff --git a/lib/inference/open_ai_image_generator.rb b/lib/inference/open_ai_image_generator.rb
deleted file mode 100644
index 83501989..00000000
--- a/lib/inference/open_ai_image_generator.rb
+++ /dev/null
@@ -1,469 +0,0 @@
-# frozen_string_literal: true
-
-module ::DiscourseAi
- module Inference
- class OpenAiImageGenerator
- TIMEOUT = 60
- MAX_IMAGE_SIZE = 20_971_520 # 20MB (technically 25 is supported by API)
-
- def self.create_uploads!(
- prompts,
- model:,
- size: nil,
- api_key: nil,
- api_url: nil,
- user_id:,
- for_private_message: false,
- n: 1,
- quality: nil,
- style: nil,
- background: nil,
- moderation: "low",
- output_compression: nil,
- output_format: nil,
- title: nil,
- cancel_manager: nil
- )
- # Get the API responses in parallel threads
- api_responses =
- generate_images_in_threads(
- prompts,
- model: model,
- size: size,
- api_key: api_key,
- api_url: api_url,
- n: n,
- quality: quality,
- style: style,
- background: background,
- moderation: moderation,
- output_compression: output_compression,
- output_format: output_format,
- cancel_manager: cancel_manager,
- )
-
- raise api_responses[0] if api_responses.all? { |resp| resp.is_a?(StandardError) }
-
- api_responses = api_responses.filter { |response| !response.is_a?(StandardError) }
-
- create_uploads_from_responses(api_responses, user_id, for_private_message, title)
- end
-
- # Method for image editing that returns Upload objects
- def self.create_edited_upload!(
- images,
- prompt,
- model: "gpt-image-1",
- size: "auto",
- api_key: nil,
- api_url: nil,
- user_id:,
- for_private_message: false,
- n: 1,
- quality: nil,
- cancel_manager: nil
- )
- api_response =
- edit_images(
- images,
- prompt,
- model: model,
- size: size,
- api_key: api_key,
- api_url: api_url,
- n: n,
- quality: quality,
- cancel_manager: cancel_manager,
- )
-
- create_uploads_from_responses([api_response], user_id, for_private_message).first
- end
-
- # Common method to create uploads from API responses
- def self.create_uploads_from_responses(
- api_responses,
- user_id,
- for_private_message,
- title = nil
- )
- all_uploads = []
-
- api_responses.each do |response|
- next unless response
-
- response[:data].each_with_index do |image, index|
- Tempfile.create("ai_image_#{index}.png") do |file|
- file.binmode
- file.write(Base64.decode64(image[:b64_json]))
- file.rewind
-
- upload =
- UploadCreator.new(
- file,
- title || "image.png",
- for_private_message: for_private_message,
- ).create_for(user_id)
-
- all_uploads << {
- # Use revised_prompt if available (DALL-E 3), otherwise use original prompt
- prompt: image[:revised_prompt] || response[:original_prompt],
- upload: upload,
- }
- end
- end
- end
-
- all_uploads
- end
-
- def self.generate_images_in_threads(
- prompts,
- model:,
- size:,
- api_key:,
- api_url:,
- n:,
- quality:,
- style:,
- background:,
- moderation:,
- output_compression:,
- output_format:,
- cancel_manager:
- )
- prompts = [prompts] unless prompts.is_a?(Array)
- prompts = prompts.take(4) # Limit to 4 prompts max
-
- # Use provided values or defaults
- api_key ||= SiteSetting.ai_openai_api_key
- api_url ||= SiteSetting.ai_openai_image_generation_url
-
- # Thread processing
- threads = []
- prompts.each do |prompt|
- threads << Thread.new(prompt) do |inner_prompt|
- attempts = 0
- begin
- perform_generation_api_call!(
- inner_prompt,
- model: model,
- size: size,
- api_key: api_key,
- api_url: api_url,
- n: n,
- quality: quality,
- style: style,
- background: background,
- moderation: moderation,
- output_compression: output_compression,
- output_format: output_format,
- cancel_manager: cancel_manager,
- )
- rescue => e
- attempts += 1
- # to keep tests speedy
- if !Rails.env.test? && !cancel_manager&.cancelled?
- retry if attempts < 3
- end
- if !cancel_manager&.cancelled?
- Discourse.warn_exception(
- e,
- message: "Failed to generate image for prompt #{prompt}\n",
- )
- puts "Error generating image for prompt: #{prompt} #{e}" if Rails.env.development?
- end
- e
- end
- end
- end
-
- threads.each(&:join)
- threads.filter_map(&:value)
- end
-
- def self.edit_images(
- images,
- prompt,
- model: "gpt-image-1",
- size: "auto",
- api_key: nil,
- api_url: nil,
- n: 1,
- quality: nil,
- cancel_manager: nil
- )
- images = [images] if !images.is_a?(Array)
-
- # For dall-e-2, only one image is supported
- if model == "dall-e-2" && images.length > 1
- raise "DALL-E 2 only supports editing one image at a time"
- end
-
- # For gpt-image-1, limit to 16 images
- images = images.take(16) if model == "gpt-image-1" && images.length > 16
-
- # Use provided values or defaults
- api_key ||= SiteSetting.ai_openai_api_key
- api_url ||= SiteSetting.ai_openai_image_edit_url
-
- # Execute edit API call
- attempts = 0
- begin
- perform_edit_api_call!(
- images,
- prompt,
- model: model,
- size: size,
- api_key: api_key,
- api_url: api_url,
- n: n,
- quality: quality,
- cancel_manager: cancel_manager,
- )
- rescue => e
- raise e if cancel_manager&.cancelled?
- attempts += 1
- if !Rails.env.test?
- sleep 2
- retry if attempts < 3
- end
- if Rails.env.development?
- puts "Error editing image(s) with prompt: #{prompt} #{e}"
- p e
- end
- Discourse.warn_exception(e, message: "Failed to edit image(s) with prompt #{prompt}")
- raise e
- end
- end
-
- # Image generation API call method
- def self.perform_generation_api_call!(
- prompt,
- model:,
- size: nil,
- api_key: nil,
- api_url: nil,
- n: 1,
- quality: nil,
- style: nil,
- background: nil,
- moderation: nil,
- output_compression: nil,
- output_format: nil,
- cancel_manager: nil
- )
- api_key ||= SiteSetting.ai_openai_api_key
- api_url ||= SiteSetting.ai_openai_image_generation_url
-
- uri = URI(api_url)
- headers = { "Content-Type" => "application/json" }
-
- if uri.host.include?("azure")
- headers["api-key"] = api_key
- else
- headers["Authorization"] = "Bearer #{api_key}"
- end
-
- # Build payload based on model type
- payload = { model: model, prompt: prompt, n: n }
-
- # Add model-specific parameters
- if model == "gpt-image-1"
- if size
- payload[:size] = size
- else
- payload[:size] = "auto"
- end
- payload[:background] = background if background
- payload[:moderation] = moderation if moderation
- payload[:output_compression] = output_compression if output_compression
- payload[:output_format] = output_format if output_format
- payload[:quality] = quality if quality
- elsif model.start_with?("dall")
- payload[:size] = size || "1024x1024"
- payload[:quality] = quality || "hd"
- payload[:style] = style if style
- payload[:response_format] = "b64_json"
- end
-
- # Store original prompt for upload metadata
- original_prompt = prompt
- cancel_manager_callback = nil
-
- FinalDestination::HTTP.start(
- uri.host,
- uri.port,
- use_ssl: uri.scheme == "https",
- read_timeout: TIMEOUT,
- open_timeout: TIMEOUT,
- write_timeout: TIMEOUT,
- ) do |http|
- request = Net::HTTP::Post.new(uri, headers)
- request.body = payload.to_json
-
- if cancel_manager
- cancel_manager_callback = lambda { http.finish }
- cancel_manager.add_callback(cancel_manager_callback)
- end
-
- json = nil
- http.request(request) do |response|
- if response.code.to_i != 200
- raise "OpenAI API returned #{response.code} #{response.body}"
- else
- json = JSON.parse(response.body, symbolize_names: true)
- # Add original prompt to response to preserve it
- json[:original_prompt] = original_prompt
- end
- end
- json
- end
- ensure
- if cancel_manager && cancel_manager_callback
- cancel_manager.remove_callback(cancel_manager_callback)
- end
- end
-
- def self.perform_edit_api_call!(
- images,
- prompt,
- model: "gpt-image-1",
- size: "auto",
- api_key:,
- api_url:,
- n: 1,
- quality: nil,
- cancel_manager: nil
- )
- uri = URI(api_url)
-
- # Setup for multipart/form-data request
- boundary = SecureRandom.hex
- headers = { "Content-Type" => "multipart/form-data; boundary=#{boundary}" }
-
- if uri.host.include?("azure")
- headers["api-key"] = api_key
- else
- headers["Authorization"] = "Bearer #{api_key}"
- end
-
- # Create multipart form data
- body = []
-
- # Add model
- body << "--#{boundary}\r\n"
- body << "Content-Disposition: form-data; name=\"model\"\r\n\r\n"
-
- body << "#{model}\r\n"
-
- files_to_delete = []
-
- # Add images
- images.each do |image|
- image_data = nil
- image_filename = nil
-
- # Handle different image input types
- if image.is_a?(Upload)
- image_path =
- if image.local?
- Discourse.store.path_for(image)
- else
- filename =
- Discourse.store.download_safe(image, max_file_size_kb: MAX_IMAGE_SIZE)&.path
- files_to_delete << filename if filename
- filename
- end
- image_data = File.read(image_path)
- image_filename = File.basename(image.url)
- else
- raise "Unsupported image format. Must be an Upload"
- end
-
- body << "--#{boundary}\r\n"
- body << "Content-Disposition: form-data; name=\"image[]\"; filename=\"#{image_filename}\"\r\n"
- body << "Content-Type: image/png\r\n\r\n"
- body << image_data
- body << "\r\n"
- end
-
- # Add prompt
- body << "--#{boundary}\r\n"
- body << "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n"
- body << "#{prompt}\r\n"
-
- # Add size if provided
- if size
- body << "--#{boundary}\r\n"
- body << "Content-Disposition: form-data; name=\"size\"\r\n\r\n"
- body << "#{size}\r\n"
- end
-
- # Add n if provided and not the default
- if n != 1
- body << "--#{boundary}\r\n"
- body << "Content-Disposition: form-data; name=\"n\"\r\n\r\n"
- body << "#{n}\r\n"
- end
-
- # Add quality if provided
- if quality
- body << "--#{boundary}\r\n"
- body << "Content-Disposition: form-data; name=\"quality\"\r\n\r\n"
- body << "#{quality}\r\n"
- end
-
- # Add response_format if provided
- if model.start_with?("dall")
- # Default to b64_json for consistency with generation
- body << "--#{boundary}\r\n"
- body << "Content-Disposition: form-data; name=\"response_format\"\r\n\r\n"
- body << "b64_json\r\n"
- end
-
- # End boundary
- body << "--#{boundary}--\r\n"
-
- # Store original prompt for upload metadata
- original_prompt = prompt
- cancel_manager_callback = nil
-
- FinalDestination::HTTP.start(
- uri.host,
- uri.port,
- use_ssl: uri.scheme == "https",
- read_timeout: TIMEOUT,
- open_timeout: TIMEOUT,
- write_timeout: TIMEOUT,
- ) do |http|
- request = Net::HTTP::Post.new(uri.path, headers)
- request.body = body.join
-
- if cancel_manager
- cancel_manager_callback = lambda { http.finish }
- cancel_manager.add_callback(cancel_manager_callback)
- end
-
- json = nil
- http.request(request) do |response|
- if response.code.to_i != 200
- raise "OpenAI API returned #{response.code} #{response.body}"
- else
- json = JSON.parse(response.body, symbolize_names: true)
- # Add original prompt to response to preserve it
- json[:original_prompt] = original_prompt
- end
- end
- json
- end
- ensure
- if cancel_manager && cancel_manager_callback
- cancel_manager.remove_callback(cancel_manager_callback)
- end
- if files_to_delete.present?
- files_to_delete.each { |file| File.delete(file) if File.exist?(file) }
- end
- end
- end
- end
-end
diff --git a/lib/inference/stability_generator.rb b/lib/inference/stability_generator.rb
deleted file mode 100644
index 124f5d8a..00000000
--- a/lib/inference/stability_generator.rb
+++ /dev/null
@@ -1,160 +0,0 @@
-# frozen_string_literal: true
-
-module ::DiscourseAi
- module Inference
- class StabilityGenerator
- TIMEOUT = 120
-
- # there is a new api for sd3
- def self.perform_sd3!(
- prompt,
- aspect_ratio: nil,
- api_key: nil,
- engine: nil,
- api_url: nil,
- output_format: "png",
- seed: nil
- )
- api_key ||= SiteSetting.ai_stability_api_key
- engine ||= SiteSetting.ai_stability_engine
- api_url ||= SiteSetting.ai_stability_api_url
-
- allowed_ratios = %w[16:9 1:1 21:9 2:3 3:2 4:5 5:4 9:16 9:21]
-
- aspect_ratio = "1:1" if !aspect_ratio || !allowed_ratios.include?(aspect_ratio)
-
- payload = {
- prompt: prompt,
- mode: "text-to-image",
- model: engine,
- output_format: output_format,
- aspect_ratio: aspect_ratio,
- }
-
- payload[:seed] = seed if seed
-
- endpoint = "v2beta/stable-image/generate/sd3"
-
- form_data = payload.to_a.map { |k, v| [k.to_s, v.to_s] }
-
- uri = URI("#{api_url}/#{endpoint}")
- request = FinalDestination::HTTP::Post.new(uri)
-
- request["authorization"] = "Bearer #{api_key}"
- request["accept"] = "application/json"
- request["User-Agent"] = DiscourseAi::AiBot::USER_AGENT
- request.set_form form_data, "multipart/form-data"
-
- response =
- FinalDestination::HTTP.start(
- uri.hostname,
- uri.port,
- use_ssl: uri.port != 80,
- read_timeout: TIMEOUT,
- open_timeout: TIMEOUT,
- write_timeout: TIMEOUT,
- ) { |http| http.request(request) }
-
- if response.code != "200"
- Rails.logger.error(
- "AI stability generator failed with status #{response.code}: #{response.body}}",
- )
- raise Net::HTTPBadResponse
- end
-
- parsed = JSON.parse(response.body, symbolize_names: true)
-
- # remap to old format
- { artifacts: [{ base64: parsed[:image], seed: parsed[:seed] }] }
- end
-
- def self.perform!(
- prompt,
- aspect_ratio: nil,
- api_key: nil,
- engine: nil,
- api_url: nil,
- image_count: 4,
- seed: nil
- )
- api_key ||= SiteSetting.ai_stability_api_key
- engine ||= SiteSetting.ai_stability_engine
- api_url ||= SiteSetting.ai_stability_api_url
-
- image_count = 4 if image_count > 4
-
- if engine.start_with? "sd3"
- artifacts =
- image_count.times.map do
- perform_sd3!(
- prompt,
- api_key: api_key,
- engine: engine,
- api_url: api_url,
- aspect_ratio: aspect_ratio,
- seed: seed,
- )[
- :artifacts
- ][
- 0
- ]
- end
-
- return { artifacts: artifacts }
- end
-
- headers = {
- "Content-Type" => "application/json",
- "Accept" => "application/json",
- "Authorization" => "Bearer #{api_key}",
- }
-
- ratio_to_dimension = {
- "16:9" => [1536, 640],
- "1:1" => [1024, 1024],
- "21:9" => [1344, 768],
- "2:3" => [896, 1152],
- "3:2" => [1152, 896],
- "4:5" => [832, 1216],
- "5:4" => [1216, 832],
- "9:16" => [640, 1536],
- "9:21" => [768, 1344],
- }
-
- if engine.include? "xl"
- width, height = ratio_to_dimension[aspect_ratio] if aspect_ratio
-
- width, height = [1024, 1024] if !width || !height
- else
- width, height = [512, 512]
- end
-
- payload = {
- text_prompts: [{ text: prompt }],
- cfg_scale: 7,
- clip_guidance_preset: "FAST_BLUE",
- height: width,
- width: height,
- samples: image_count,
- steps: 30,
- }
-
- payload[:seed] = seed if seed
-
- endpoint = "v1/generation/#{engine}/text-to-image"
-
- conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
- response = conn.post("#{api_url}/#{endpoint}", payload.to_json, headers)
-
- if response.status != 200
- Rails.logger.error(
- "AI stability generator failed with status #{response.status}: #{response.body}}",
- )
- raise Net::HTTPBadResponse
- end
-
- JSON.parse(response.body, symbolize_names: true)
- end
- end
- end
-end
diff --git a/lib/inferred_concepts/applier.rb b/lib/inferred_concepts/applier.rb
deleted file mode 100644
index ca8ff58c..00000000
--- a/lib/inferred_concepts/applier.rb
+++ /dev/null
@@ -1,135 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module InferredConcepts
- class Applier
- # Associates the provided concepts with a topic
- # topic: a Topic instance
- # concepts: an array of InferredConcept instances
- def apply_to_topic(topic, concepts)
- return if topic.blank? || concepts.blank?
-
- topic.inferred_concepts << concepts
- end
-
- # Associates the provided concepts with a post
- # post: a Post instance
- # concepts: an array of InferredConcept instances
- def apply_to_post(post, concepts)
- return if post.blank? || concepts.blank?
-
- post.inferred_concepts << concepts
- end
-
- # Extracts content from a topic for concept analysis
- # Returns a string with the topic title and first few posts
- def topic_content_for_analysis(topic)
- return "" if topic.blank?
-
- # Combine title and first few posts for analysis
- posts = Post.where(topic_id: topic.id).order(:post_number).limit(10)
-
- content = "Title: #{topic.title}\n\n"
- content += posts.map { |p| "#{p.post_number}) #{p.user.username}: #{p.raw}" }.join("\n\n")
-
- content
- end
-
- # Extracts content from a post for concept analysis
- # Returns a string with the post content
- def post_content_for_analysis(post)
- return "" if post.blank?
-
- # Get the topic title for context
- topic_title = post.topic&.title || ""
-
- content = "Topic: #{topic_title}\n\n"
- content += "Post by #{post.user.username}:\n#{post.raw}"
-
- content
- end
-
- # Match a topic with existing concepts
- def match_existing_concepts(topic)
- return [] if topic.blank?
-
- # Get content to analyze
- content = topic_content_for_analysis(topic)
-
- # Get all existing concepts
- existing_concepts = DiscourseAi::InferredConcepts::Manager.new.list_concepts
- return [] if existing_concepts.empty?
-
- # Use the ConceptMatcher persona to match concepts
- matched_concept_names = match_concepts_to_content(content, existing_concepts)
-
- # Find concepts in the database
- matched_concepts = InferredConcept.where(name: matched_concept_names)
-
- # Apply concepts to the topic
- apply_to_topic(topic, matched_concepts)
-
- matched_concepts
- end
-
- # Match a post with existing concepts
- def match_existing_concepts_for_post(post)
- return [] if post.blank?
-
- # Get content to analyze
- content = post_content_for_analysis(post)
-
- # Get all existing concepts
- existing_concepts = DiscourseAi::InferredConcepts::Manager.new.list_concepts
- return [] if existing_concepts.empty?
-
- # Use the ConceptMatcher persona to match concepts
- matched_concept_names = match_concepts_to_content(content, existing_concepts)
-
- # Find concepts in the database
- matched_concepts = InferredConcept.where(name: matched_concept_names)
-
- # Apply concepts to the post
- apply_to_post(post, matched_concepts)
-
- matched_concepts
- end
-
- # Use ConceptMatcher persona to match content against provided concepts
- def match_concepts_to_content(content, concept_list)
- return [] if content.blank? || concept_list.blank?
-
- # Prepare user message with only the content
- user_message = content
-
- # Use the ConceptMatcher persona to match concepts
-
- persona =
- AiPersona
- .all_personas(enabled_only: false)
- .find { |p| p.id == SiteSetting.inferred_concepts_match_persona.to_i }
- .new
-
- llm = LlmModel.find(persona.class.default_llm_id)
-
- input = { type: :user, content: content }
-
- context =
- DiscourseAi::Personas::BotContext.new(
- messages: [input],
- user: Discourse.system_user,
- inferred_concepts: concept_list,
- )
-
- bot = DiscourseAi::Personas::Bot.as(Discourse.system_user, persona: persona, model: llm)
- structured_output = nil
-
- bot.reply(context) do |partial, _, type|
- structured_output = partial if type == :structured_output
- end
-
- structured_output&.read_buffered_property(:matching_concepts) || []
- end
- end
- end
-end
diff --git a/lib/inferred_concepts/finder.rb b/lib/inferred_concepts/finder.rb
deleted file mode 100644
index 9e1466f5..00000000
--- a/lib/inferred_concepts/finder.rb
+++ /dev/null
@@ -1,176 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module InferredConcepts
- class Finder
- # Identifies potential concepts from provided content
- # Returns an array of concept names (strings)
- def identify_concepts(content)
- return [] if content.blank?
-
- # Use the ConceptFinder persona to identify concepts
- persona =
- AiPersona
- .all_personas(enabled_only: false)
- .find { |p| p.id == SiteSetting.inferred_concepts_generate_persona.to_i }
- .new
-
- llm = LlmModel.find(persona.class.default_llm_id)
- context =
- DiscourseAi::Personas::BotContext.new(
- messages: [{ type: :user, content: content }],
- user: Discourse.system_user,
- inferred_concepts: DiscourseAi::InferredConcepts::Manager.new.list_concepts,
- )
-
- bot = DiscourseAi::Personas::Bot.as(Discourse.system_user, persona: persona, model: llm)
- structured_output = nil
-
- bot.reply(context) do |partial, _, type|
- structured_output = partial if type == :structured_output
- end
-
- structured_output&.read_buffered_property(:concepts) || []
- end
-
- # Creates or finds concepts in the database from provided names
- # Returns an array of InferredConcept instances
- def create_or_find_concepts(concept_names)
- return [] if concept_names.blank?
-
- concept_names.map { |name| InferredConcept.find_or_create_by(name: name) }
- end
-
- # Finds candidate topics to use for concept generation
- #
- # @param limit [Integer] Maximum number of topics to return
- # @param min_posts [Integer] Minimum number of posts in topic
- # @param min_likes [Integer] Minimum number of likes across all posts
- # @param min_views [Integer] Minimum number of views
- # @param exclude_topic_ids [Array] Topic IDs to exclude
- # @param category_ids [Array] Only include topics from these categories (optional)
- # @param created_after [DateTime] Only include topics created after this time (optional)
- # @return [Array] Array of Topic objects that are good candidates
- def find_candidate_topics(
- limit: 100,
- min_posts: 5,
- min_likes: 10,
- min_views: 100,
- exclude_topic_ids: [],
- category_ids: nil,
- created_after: 30.days.ago
- )
- query =
- Topic.where(
- "topics.posts_count >= ? AND topics.views >= ? AND topics.like_count >= ?",
- min_posts,
- min_views,
- min_likes,
- )
-
- # Apply additional filters
- query = query.where("topics.id NOT IN (?)", exclude_topic_ids) if exclude_topic_ids.present?
- query = query.where("topics.category_id IN (?)", category_ids) if category_ids.present?
- query = query.where("topics.created_at >= ?", created_after) if created_after.present?
-
- # Exclude PM topics (if they exist in Discourse)
- query = query.where(archetype: Archetype.default)
-
- # Exclude topics that already have concepts
- topics_with_concepts = <<~SQL
- SELECT DISTINCT topic_id
- FROM inferred_concept_topics
- SQL
-
- query = query.where("topics.id NOT IN (#{topics_with_concepts})")
-
- # Score and order topics by engagement (combination of views, likes, and posts)
- query =
- query.select(
- "topics.*,
- (topics.like_count * 2 + topics.posts_count * 3 + topics.views * 0.1) AS engagement_score",
- ).order("engagement_score DESC")
-
- # Return limited number of topics
- query.limit(limit)
- end
-
- # Find candidate posts that are good for concept generation
- #
- # @param limit [Integer] Maximum number of posts to return
- # @param min_likes [Integer] Minimum number of likes
- # @param exclude_first_posts [Boolean] Exclude first posts in topics
- # @param exclude_post_ids [Array] Post IDs to exclude
- # @param category_ids [Array] Only include posts from topics in these categories
- # @param created_after [DateTime] Only include posts created after this time
- # @return [Array] Array of Post objects that are good candidates
- def find_candidate_posts(
- limit: 100,
- min_likes: 5,
- exclude_first_posts: true,
- exclude_post_ids: [],
- category_ids: nil,
- created_after: 30.days.ago
- )
- query = Post.where("posts.like_count >= ?", min_likes)
-
- # Exclude first posts if specified
- query = query.where("posts.post_number > 1") if exclude_first_posts
-
- # Apply additional filters
- query = query.where("posts.id NOT IN (?)", exclude_post_ids) if exclude_post_ids.present?
- query = query.where("posts.created_at >= ?", created_after) if created_after.present?
-
- # Filter by category if specified
- if category_ids.present?
- query = query.joins(:topic).where("topics.category_id IN (?)", category_ids)
- end
-
- # Exclude posts that already have concepts
- posts_with_concepts = <<~SQL
- SELECT DISTINCT post_id
- FROM inferred_concept_posts
- SQL
-
- query = query.where("posts.id NOT IN (#{posts_with_concepts})")
-
- # Order by engagement (likes)
- query = query.order(like_count: :desc)
-
- # Return limited number of posts
- query.limit(limit)
- end
-
- # Deduplicate and standardize a list of concepts
- # @param concept_names [Array] List of concept names to deduplicate
- # @return [Hash] Hash with deduplicated concepts and mapping
- def deduplicate_concepts(concept_names)
- return { deduplicated_concepts: [], mapping: {} } if concept_names.blank?
-
- # Use the ConceptDeduplicator persona to deduplicate concepts
- persona =
- AiPersona
- .all_personas(enabled_only: false)
- .find { |p| p.id == SiteSetting.inferred_concepts_deduplicate_persona.to_i }
- .new
-
- llm = LlmModel.find(persona.class.default_llm_id)
-
- # Create the input for the deduplicator
- input = { type: :user, content: concept_names.join(", ") }
-
- context =
- DiscourseAi::Personas::BotContext.new(messages: [input], user: Discourse.system_user)
-
- bot = DiscourseAi::Personas::Bot.as(Discourse.system_user, persona: persona, model: llm)
- structured_output = nil
-
- bot.reply(context) do |partial, _, type|
- structured_output = partial if type == :structured_output
- end
-
- structured_output&.read_buffered_property(:streamlined_tags) || []
- end
- end
- end
-end
diff --git a/lib/inferred_concepts/manager.rb b/lib/inferred_concepts/manager.rb
deleted file mode 100644
index 5ac96694..00000000
--- a/lib/inferred_concepts/manager.rb
+++ /dev/null
@@ -1,201 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module InferredConcepts
- class Manager
- # Get a list of existing concepts
- # @param limit [Integer, nil] Optional maximum number of concepts to return
- # @return [Array] Array of InferredConcept objects
- def list_concepts(limit: nil)
- query = InferredConcept.all.order("name ASC")
-
- # Apply limit if provided
- query = query.limit(limit) if limit.present?
-
- query.pluck(:name)
- end
-
- # Deduplicate concepts in batches by letter
- # This method will:
- # 1. Group concepts by first letter
- # 2. Process each letter group separately through the deduplicator
- # 3. Do a final pass with all deduplicated concepts
- # @return [Hash] Statistics about the deduplication process
- def deduplicate_concepts_by_letter(per_letter_batch: 50, full_pass_batch: 150)
- # Get all concepts
- all_concepts = list_concepts
- return if all_concepts.empty?
-
- letter_groups = Hash.new { |h, k| h[k] = [] }
-
- # Group concepts by first letter
- all_concepts.each do |concept|
- first_char = concept[0]&.upcase
-
- if first_char && first_char.match?(/[A-Z]/)
- letter_groups[first_char] << concept
- else
- # Non-alphabetic or empty concepts go in a special group
- letter_groups["#"] << concept
- end
- end
-
- # Process each letter group
- letter_deduplicated_concepts = []
- finder = DiscourseAi::InferredConcepts::Finder.new
-
- letter_groups.each do |letter, concepts|
- next if concepts.empty?
-
- batches = concepts.each_slice(per_letter_batch).to_a
-
- batches.each do |batch|
- result = finder.deduplicate_concepts(batch)
- letter_deduplicated_concepts.concat(result)
- end
- end
-
- # Final pass with all deduplicated concepts
- if letter_deduplicated_concepts.present?
- final_result = []
-
- batches = letter_deduplicated_concepts.each_slice(full_pass_batch).to_a
- batches.each do |batch|
- dedups = finder.deduplicate_concepts(batch)
- final_result.concat(dedups)
- end
-
- # Remove duplicates
- final_result.uniq!
-
- # Apply the deduplicated concepts
- InferredConcept.where.not(name: final_result).destroy_all
- InferredConcept.insert_all(final_result.map { |concept| { name: concept } })
- end
- end
-
- # Extract new concepts from arbitrary content
- # @param content [String] The content to analyze
- # @return [Array] The identified concept names
- def identify_concepts(content)
- DiscourseAi::InferredConcepts::Finder.new.identify_concepts(content)
- end
-
- # Identify and create concepts from content without applying them to any topic
- # @param content [String] The content to analyze
- # @return [Array] The created or found concepts
- def generate_concepts_from_content(content)
- return [] if content.blank?
-
- # Identify concepts
- finder = DiscourseAi::InferredConcepts::Finder.new
- concept_names = finder.identify_concepts(content)
- return [] if concept_names.blank?
-
- # Create or find concepts in the database
- finder.create_or_find_concepts(concept_names)
- end
-
- # Generate concepts from a topic's content without applying them to the topic
- # @param topic [Topic] A Topic instance
- # @return [Array] The created or found concepts
- def generate_concepts_from_topic(topic)
- return [] if topic.blank?
-
- # Get content to analyze
- applier = DiscourseAi::InferredConcepts::Applier.new
- content = applier.topic_content_for_analysis(topic)
- return [] if content.blank?
-
- # Generate concepts from the content
- generate_concepts_from_content(content)
- end
-
- # Generate concepts from a post's content without applying them to the post
- # @param post [Post] A Post instance
- # @return [Array] The created or found concepts
- def generate_concepts_from_post(post)
- return [] if post.blank?
-
- # Get content to analyze
- applier = DiscourseAi::InferredConcepts::Applier.new
- content = applier.post_content_for_analysis(post)
- return [] if content.blank?
-
- # Generate concepts from the content
- generate_concepts_from_content(content)
- end
-
- # Match a topic against existing concepts
- # @param topic [Topic] A Topic instance
- # @return [Array] The concepts that were applied
- def match_topic_to_concepts(topic)
- return [] if topic.blank?
-
- DiscourseAi::InferredConcepts::Applier.new.match_existing_concepts(topic)
- end
-
- # Match a post against existing concepts
- # @param post [Post] A Post instance
- # @return [Array] The concepts that were applied
- def match_post_to_concepts(post)
- return [] if post.blank?
-
- DiscourseAi::InferredConcepts::Applier.new.match_existing_concepts_for_post(post)
- end
-
- # Find topics that have a specific concept
- # @param concept_name [String] The name of the concept to search for
- # @return [Array] Topics that have the specified concept
- def search_topics_by_concept(concept_name)
- concept = ::InferredConcept.find_by(name: concept_name)
- return [] unless concept
- concept.topics
- end
-
- # Find posts that have a specific concept
- # @param concept_name [String] The name of the concept to search for
- # @return [Array] Posts that have the specified concept
- def search_posts_by_concept(concept_name)
- concept = ::InferredConcept.find_by(name: concept_name)
- return [] unless concept
- concept.posts
- end
-
- # Match arbitrary content against existing concepts
- # @param content [String] The content to analyze
- # @return [Array] Names of matching concepts
- def match_content_to_concepts(content)
- existing_concepts = InferredConcept.all.pluck(:name)
- return [] if existing_concepts.empty?
-
- DiscourseAi::InferredConcepts::Applier.new.match_concepts_to_content(
- content,
- existing_concepts,
- )
- end
-
- # Find candidate topics that are good for concept generation
- #
- # @param opts [Hash] Options to pass to the finder
- # @option opts [Integer] :limit (100) Maximum number of topics to return
- # @option opts [Integer] :min_posts (5) Minimum number of posts in topic
- # @option opts [Integer] :min_likes (10) Minimum number of likes across all posts
- # @option opts [Integer] :min_views (100) Minimum number of views
- # @option opts [Array] :exclude_topic_ids ([]) Topic IDs to exclude
- # @option opts [Array] :category_ids (nil) Only include topics from these categories
- # @option opts [DateTime] :created_after (30.days.ago) Only include topics created after this time
- # @return [Array] Array of Topic objects that are good candidates
- def find_candidate_topics(opts = {})
- DiscourseAi::InferredConcepts::Finder.new.find_candidate_topics(**opts)
- end
-
- # Find candidate posts that are good for concept generation
- # @param opts [Hash] Options to pass to the finder
- # @return [Array] Array of Post objects that are good candidates
- def find_candidate_posts(opts = {})
- DiscourseAi::InferredConcepts::Finder.new.find_candidate_posts(**opts)
- end
- end
- end
-end
diff --git a/lib/multisite_hash.rb b/lib/multisite_hash.rb
deleted file mode 100644
index 16cf646a..00000000
--- a/lib/multisite_hash.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- class MultisiteHash
- def initialize(id)
- @hash = Hash.new { |h, k| h[k] = {} }
- @id = id
-
- MessageBus.subscribe(channel_name) { |message| @hash[message.data] = {} }
- end
-
- def channel_name
- "/multisite-hash-#{@id}"
- end
-
- def current_db
- RailsMultisite::ConnectionManagement.current_db
- end
-
- def fetch(key)
- @hash[current_db][key] ||= yield
- end
-
- def [](key)
- @hash.dig(current_db, key)
- end
-
- def []=(key, val)
- @hash[current_db][key] = val
- end
-
- def flush!
- @hash[current_db] = {}
- MessageBus.publish(channel_name, current_db)
- end
-
- # TODO implement a GC so we don't retain too much memory
- end
-end
diff --git a/lib/personas/artifact_update_strategies/base.rb b/lib/personas/artifact_update_strategies/base.rb
deleted file mode 100644
index 1a1c0ecb..00000000
--- a/lib/personas/artifact_update_strategies/base.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi
- module Personas
- module ArtifactUpdateStrategies
- class InvalidFormatError < StandardError
- end
- class Base
- attr_reader :post, :user, :artifact, :artifact_version, :instructions, :llm, :cancel_manager
-
- def initialize(
- llm:,
- post:,
- user:,
- artifact:,
- artifact_version:,
- instructions:,
- cancel_manager: nil
- )
- @llm = llm
- @post = post
- @user = user
- @artifact = artifact
- @artifact_version = artifact_version
- @instructions = instructions
- @cancel_manager = cancel_manager
- end
-
- def apply(&progress)
- changes = generate_changes(&progress)
- parsed_changes = parse_changes(changes)
- apply_changes(parsed_changes)
- end
-
- def storage_api
- if @artifact.metadata.is_a?(Hash) && @artifact.metadata["requires_storage"]
- DiscourseAi::Personas::Tools::CreateArtifact.storage_api
- end
- end
-
- private
-
- def generate_changes(&progress)
- response = +""
- llm.generate(build_prompt, user: user, cancel_manager: cancel_manager) do |partial|
- progress.call(partial) if progress
- response << partial
- end
- response
- end
-
- def build_prompt
- # To be implemented by subclasses
- raise NotImplementedError
- end
-
- def parse_changes(response)
- # To be implemented by subclasses
- raise NotImplementedError
- end
-
- def apply_changes(changes)
- # To be implemented by subclasses
- raise NotImplementedError
- end
- end
- end
- end
-end
diff --git a/lib/personas/artifact_update_strategies/diff.rb b/lib/personas/artifact_update_strategies/diff.rb
deleted file mode 100644
index fb078691..00000000
--- a/lib/personas/artifact_update_strategies/diff.rb
+++ /dev/null
@@ -1,300 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi
- module Personas
- module ArtifactUpdateStrategies
- class Diff < Base
- attr_reader :failed_searches
-
- private
-
- def initialize(**kwargs)
- super
- @failed_searches = []
- end
-
- def build_prompt
- DiscourseAi::Completions::Prompt.new(
- system_prompt,
- messages: [{ type: :user, content: user_prompt }],
- post_id: post.id,
- topic_id: post.topic_id,
- )
- end
-
- def parse_changes(response)
- sections = { html: nil, css: nil, javascript: nil }
- current_section = nil
- lines = []
-
- response.each_line do |line|
- case line
- when /^\[(HTML|CSS|JavaScript)\]$/
- sections[current_section] = lines.join if current_section && !lines.empty?
- current_section = line.match(/^\[(.+)\]$/)[1].downcase.to_sym
- lines = []
- when %r{^\[/(?:HTML|CSS|JavaScript)\]$}
- sections[current_section] = lines.join if current_section && !lines.empty?
- current_section = nil
- else
- lines << line if current_section
- end
- end
-
- sections.each do |section, content|
- sections[section] = extract_search_replace_blocks(content)
- end
-
- sections
- end
-
- def apply_changes(changes)
- source = artifact_version || artifact
- updated_content = { js: source.js, html: source.html, css: source.css }
-
- %i[html css javascript].each do |section|
- blocks = changes[section]
- next unless blocks
-
- content = source.public_send(section == :javascript ? :js : section)
- blocks.each do |block|
- begin
- if !block[:search]
- content = block[:replace]
- else
- content =
- DiscourseAi::Utils::DiffUtils::SimpleDiff.apply(
- content,
- block[:search],
- block[:replace],
- )
- end
- rescue DiscourseAi::Utils::DiffUtils::SimpleDiff::NoMatchError
- @failed_searches << { section: section, search: block[:search] }
- # TODO, we may need to inform caller here, LLM made a mistake which it
- # should correct
- puts "Failed to find search: #{block[:search]}"
- end
- end
- updated_content[section == :javascript ? :js : section] = content
- end
-
- artifact.create_new_version(
- html: updated_content[:html],
- css: updated_content[:css],
- js: updated_content[:js],
- change_description: instructions,
- )
- end
-
- private
-
- def extract_search_replace_blocks(content)
- return nil if content.blank? || content.to_s.strip.downcase.match?(/^\(?no changes?\)?$/m)
- return [{ replace: content }] if !content.include?("<<< SEARCH")
-
- blocks = []
- current_block = {}
- state = :initial
- search_lines = []
- replace_lines = []
-
- content.each_line do |line|
- line = line.chomp
-
- case state
- when :initial
- state = :collecting_search if line.match?(/^<<<* SEARCH/)
- when :collecting_search
- if line.start_with?("===")
- current_block[:search] = search_lines.join("\n").strip
- search_lines = []
- state = :collecting_replace
- else
- search_lines << line
- end
- when :collecting_replace
- if line.match?(/>>>* REPLACE/)
- current_block[:replace] = replace_lines.join("\n").strip
- replace_lines = []
- blocks << current_block
- current_block = {}
- state = :initial
- else
- replace_lines << line
- end
- end
- end
-
- # Handle any remaining block
- if state == :collecting_replace && !replace_lines.empty?
- current_block[:replace] = replace_lines.join("\n").strip
- blocks << current_block
- end
-
- blocks.empty? ? nil : blocks
- end
-
- def system_prompt
- <<~PROMPT
- You are a web development expert generating precise search/replace changes for updating HTML, CSS, and JavaScript code.
-
- CRITICAL RULES:
-
- 1. Use EXACTLY this format for changes:
- <<<<<<< SEARCH
- (code to replace)
- =======
- (replacement code)
- >>>>>>> REPLACE
-
- 2. SEARCH blocks MUST be 8 lines or less. Break larger changes into multiple smaller search/replace blocks.
-
- 3. DO NOT modify the markers or add spaces around them.
-
- 4. DO NOT add explanations or comments within sections.
-
- 5. ONLY include [HTML], [CSS], and [JavaScript] sections if they have changes.
-
- 6. HTML should not include , , or tags, it is injected into a template.
-
- 7. NEVER EVER ask followup questions, ALL changes must be performed in a single response.
-
- 8. When performing a non-contiguous search, ALWAYS use ... to denote the skipped lines.
-
- 9. Be mindful that ... non-contiguous search is not greedy, it will only match the first occurrence.
-
- 10. Never mix a full section replacement with a search/replace block in the same section.
-
- 11. ALWAYS skip sections you do not want to change, do not include them in the response.
-
- HANDLING LARGE CHANGES:
-
- - Break large HTML structures into multiple smaller search/replace blocks.
- - Use strategic anchor points like unique IDs or class names to target specific elements.
- - Consider replacing entire components rather than modifying complex internals.
- - When elements contain dynamic content, use precise context markers or replace entire containers.
-
- VALIDATION CHECKLIST:
- - Each SEARCH block is 8 lines or less
- - Every SEARCH has exactly one matching REPLACE
- - All blocks are properly closed
- - No SEARCH/REPLACE blocks are nested
- - Each change is a complete, separate block with its own SEARCH/REPLACE markers
-
- WARNING: Never nest search/replace blocks. Each change must be a complete sequence.
-
- JavaScript libraries must be sourced from the following CDNs, otherwise CSP will reject it:
- #{AiArtifact::ALLOWED_CDN_SOURCES.join("\n")}
-
- #{storage_api}
-
- Reply Format:
- [HTML]
- (changes or empty if no changes or entire HTML)
- [/HTML]
- [CSS]
- (changes or empty if no changes or entire CSS)
- [/CSS]
- [JavaScript]
- (changes or empty if no changes or entire JavaScript)
- [/JavaScript]
-
- EXAMPLE 1 - Multiple small changes in one file:
-
- [JavaScript]
- <<<<<<< SEARCH
- console.log('old1');
- =======
- console.log('new1');
- >>>>>>> REPLACE
- <<<<<<< SEARCH
- console.log('old2');
- =======
- console.log('new2');
- >>>>>>> REPLACE
- [/JavaScript]
-
- EXAMPLE 2 - Breaking up large HTML changes:
-
- [HTML]
- <<<<<<< SEARCH
-
-
-
-
- =======
-
-
-
-
- >>>>>>> REPLACE
-
- <<<<<<< SEARCH
-
-
-
Home
-
Products
- =======
-
-
-
Home
-
Services
- >>>>>>> REPLACE
- [/HTML]
-
- EXAMPLE 3 - Non-contiguous search in CSS:
-
- [CSS]
- <<<<<<< SEARCH
- body {
- ...
- background-color: green;
- }
- =======
- body {
- color: red;
- }
- >>>>>>> REPLACE
- [/CSS]
-
- EXAMPLE 4 - Full HTML replacement:
-
- [HTML]
-
something old
-
another something old
- [/HTML]
-
- output:
-
- [HTML]
-
something new
- [/HTML]
- PROMPT
- end
-
- def user_prompt
- source = artifact_version || artifact
- <<~CONTENT
- Artifact code:
-
- [HTML]
- #{source.html}
- [/HTML]
-
- [CSS]
- #{source.css}
- [/CSS]
-
- [JavaScript]
- #{source.js}
- [/JavaScript]
-
- Instructions:
-
- #{instructions}
- CONTENT
- end
- end
- end
- end
-end
diff --git a/lib/personas/artifact_update_strategies/full.rb b/lib/personas/artifact_update_strategies/full.rb
deleted file mode 100644
index ffbf87e5..00000000
--- a/lib/personas/artifact_update_strategies/full.rb
+++ /dev/null
@@ -1,150 +0,0 @@
-# frozen_string_literal: true
-module DiscourseAi
- module Personas
- module ArtifactUpdateStrategies
- class Full < Base
- private
-
- def build_prompt
- DiscourseAi::Completions::Prompt.new(
- system_prompt,
- messages: [
- { type: :user, content: "#{current_artifact_content}\n\n\n#{instructions}" },
- ],
- post_id: post.id,
- topic_id: post.topic_id,
- )
- end
-
- def parse_changes(response)
- sections = { html: nil, css: nil, javascript: nil }
- current_section = nil
- lines = []
-
- response.each_line do |line|
- case line
- when /^\[(HTML|CSS|JavaScript)\]$/
- sections[current_section] = lines.join if current_section && !lines.empty?
- current_section = line.match(/^\[(.+)\]$/)[1].downcase.to_sym
- lines = []
- when %r{^\[/(HTML|CSS|JavaScript)\]$}
- sections[current_section] = lines.join if current_section && !lines.empty?
- current_section = nil
- lines = []
- else
- lines << line if current_section
- end
- end
-
- sections
- end
-
- def apply_changes(changes)
- source = artifact_version || artifact
- updated_content = { js: source.js, html: source.html, css: source.css }
-
- %i[html css javascript].each do |section|
- content = changes[section]&.strip
- next if content.blank?
- updated_content[section == :javascript ? :js : section] = content
- end
-
- artifact.create_new_version(
- html: updated_content[:html],
- css: updated_content[:css],
- js: updated_content[:js],
- change_description: instructions,
- )
- end
-
- private
-
- def system_prompt
- <<~PROMPT
- You are a web development expert generating updated HTML, CSS, and JavaScript code.
-
- Important rules:
- 1. Provide full source code for each changed section
- 2. Generate up to three sections: HTML, CSS, and JavaScript
- 3. Only include sections that need changes
- 4. Keep changes focused on the requirements
- 5. NEVER EVER BE LAZY, always include ALL the source code with any update you make. If you are lazy you will break the artifact.
- 6. Do not print out any reasoning, just the changed code, you will be parsed via a program.
- 7. Sections must start and end with exact tags: [HTML] [/HTML], [CSS] [/CSS], [JavaScript] [/JavaScript]
- 8. HTML should not include , , or tags, it is injected into a template
-
- JavaScript libraries must be sourced from the following CDNs, otherwise CSP will reject it:
- #{AiArtifact::ALLOWED_CDN_SOURCES.join("\n")}
-
- #{storage_api}
-
- Always adhere to the format when replying:
-
- [HTML]
- complete html code, omit if no changes
- [/HTML]
-
- [CSS]
- complete css code, omit if no changes
- [/CSS]
-
- [JavaScript]
- complete js code, omit if no changes
- [/JavaScript]
-
- Examples:
-
- Example 1 (HTML only change):
- [HTML]
-
-
Title
-
- [/HTML]
-
- Example 2 (CSS and JavaScript changes):
- [CSS]
- .container { padding: 20px; }
- .title { color: blue; }
- [/CSS]
- [JavaScript]
- function init() {
- console.log("loaded");
- }
- [/JavaScript]
-
- Example 3 (All sections):
- [HTML]
-
- [/HTML]
- [CSS]
- #app { margin: 0; }
- [/CSS]
- [JavaScript]
- const app = document.getElementById("app");
- [/JavaScript]
-
- PROMPT
- end
-
- def current_artifact_content
- source = artifact_version || artifact
- <<~CONTENT
- Current artifact code:
-
- [HTML]
- #{source.html}
- [/HTML]
-
- [CSS]
- #{source.css}
- [/CSS]
-
- [JavaScript]
- #{source.js}
- [/JavaScript]
- CONTENT
- end
- end
- end
- end
-end
diff --git a/lib/personas/artist.rb b/lib/personas/artist.rb
deleted file mode 100644
index 93e2361e..00000000
--- a/lib/personas/artist.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class Artist < Persona
- def tools
- [Tools::Image]
- end
-
- def required_tools
- [Tools::Image]
- end
-
- def system_prompt
- <<~PROMPT
- You are artistbot and you are here to help people generate images.
-
- You generate images using stable diffusion.
-
- - A good prompt needs to be detailed and specific.
- - You can specify subject, medium (e.g. oil on canvas), artist (person who drew it or photographed it)
- - You can specify details about lighting or time of day.
- - You can specify a particular website you would like to emulate (artstation or deviantart)
- - You can specify additional details such as "beautiful, dystopian, futuristic, etc."
- - Prompts should generally be 10-20 words long
- - Do not include any connector words such as "and" or "but" etc.
- - You are extremely creative, when given short non descriptive prompts from a user you add your own details
-
- - When generating images, usually opt to generate 4 images unless the user specifies otherwise.
- - Be creative with your prompts, offer diverse options
- - You can use the seeds to regenerate the same image and amend the prompt keeping general style
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/bot.rb b/lib/personas/bot.rb
deleted file mode 100644
index 12bb1079..00000000
--- a/lib/personas/bot.rb
+++ /dev/null
@@ -1,346 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class Bot
- BOT_NOT_FOUND = Class.new(StandardError)
-
- # the future is agentic, allow for more turns
- MAX_COMPLETIONS = 8
-
- # limit is arbitrary, but 5 which was used in the past was too low
- MAX_TOOLS = 20
-
- def self.as(bot_user, persona: DiscourseAi::Personas::General.new, model: nil)
- new(bot_user, persona, model)
- end
-
- def initialize(bot_user, persona, model = nil)
- @bot_user = bot_user
- @persona = persona
- @model =
- model || self.class.guess_model(bot_user) || LlmModel.find(@persona.class.default_llm_id)
- end
-
- attr_reader :bot_user, :model
- attr_accessor :persona
-
- def llm
- DiscourseAi::Completions::Llm.proxy(model)
- end
-
- def force_tool_if_needed(prompt, context)
- return if prompt.tool_choice == :none
-
- context.chosen_tools ||= []
- forced_tools = persona.force_tool_use.map { |tool| tool.name }
- force_tool = forced_tools.find { |name| !context.chosen_tools.include?(name) }
-
- if force_tool && persona.forced_tool_count > 0
- user_turns = prompt.messages.select { |m| m[:type] == :user }.length
- force_tool = false if user_turns > persona.forced_tool_count
- end
-
- if force_tool
- context.chosen_tools << force_tool
- prompt.tool_choice = force_tool
- else
- prompt.tool_choice = nil
- end
- end
-
- def reply(context, llm_args: {}, &update_blk)
- unless context.is_a?(BotContext)
- raise ArgumentError, "context must be an instance of BotContext"
- end
- context.cancel_manager ||= DiscourseAi::Completions::CancelManager.new
- current_llm = llm
- prompt = persona.craft_prompt(context, llm: current_llm)
-
- total_completions = 0
- ongoing_chain = true
- raw_context = []
-
- user = context.user
-
- llm_kwargs = llm_args.dup
- llm_kwargs[:user] = user
- llm_kwargs[:temperature] = persona.temperature if persona.temperature
- llm_kwargs[:top_p] = persona.top_p if persona.top_p
-
- if !context.bypass_response_format && persona.response_format.present?
- llm_kwargs[:response_format] = build_json_schema(persona.response_format)
- end
-
- needs_newlines = false
- tools_ran = 0
-
- while total_completions < MAX_COMPLETIONS && ongoing_chain
- tool_found = false
- force_tool_if_needed(prompt, context)
-
- tool_halted = false
-
- allow_partial_tool_calls = persona.allow_partial_tool_calls?
- existing_tools = Set.new
- current_thinking = []
-
- result =
- current_llm.generate(
- prompt,
- feature_name: context.feature_name,
- partial_tool_calls: allow_partial_tool_calls,
- output_thinking: true,
- cancel_manager: context.cancel_manager,
- **llm_kwargs,
- ) do |partial|
- tool =
- persona.find_tool(
- partial,
- bot_user: user,
- llm: current_llm,
- context: context,
- existing_tools: existing_tools,
- )
- tool = nil if tools_ran >= MAX_TOOLS
-
- if tool.present?
- existing_tools << tool
- tool_call = partial
- if tool_call.partial?
- if tool.class.allow_partial_tool_calls?
- tool.partial_invoke
- update_blk.call("", tool.custom_raw, :partial_tool)
- end
- next
- end
-
- tool_found = true
- # a bit hacky, but extra newlines do no harm
- if needs_newlines
- update_blk.call("\n\n")
- needs_newlines = false
- end
-
- process_tool(
- tool: tool,
- raw_context: raw_context,
- current_llm: current_llm,
- update_blk: update_blk,
- prompt: prompt,
- context: context,
- current_thinking: current_thinking,
- )
-
- tools_ran += 1
- ongoing_chain &&= tool.chain_next_response?
-
- tool_halted = true if !tool.chain_next_response?
- else
- next if tool_halted
- needs_newlines = true
- if partial.is_a?(DiscourseAi::Completions::ToolCall)
- Rails.logger.warn("DiscourseAi: Tool not found: #{partial.name}")
- else
- if partial.is_a?(DiscourseAi::Completions::Thinking)
- if partial.partial? && partial.message.present?
- update_blk.call(partial.message, nil, :thinking)
- end
- if !partial.partial?
- # this will be dealt with later
- raw_context << partial
- current_thinking << partial
- end
- elsif update_blk.present?
- if partial.is_a?(DiscourseAi::Completions::StructuredOutput)
- update_blk.call(partial, nil, :structured_output)
- else
- update_blk.call(partial)
- end
- end
- end
- end
- end
-
- if !tool_found
- ongoing_chain = false
- text = result
-
- # we must strip out thinking and other types of blocks
- if result.is_a?(Array)
- text = +""
- result.each { |item| text << item if item.is_a?(String) }
- end
- raw_context << [text, bot_user&.username]
- end
-
- total_completions += 1
-
- # do not allow tools when we are at the end of a chain (total_completions == MAX_COMPLETIONS - 1)
- prompt.tool_choice = :none if total_completions == MAX_COMPLETIONS - 1
- end
-
- embed_thinking(raw_context)
- end
-
- def returns_json?
- persona.response_format.present?
- end
-
- private
-
- def embed_thinking(raw_context)
- embedded_thinking = []
- thinking_info = nil
- raw_context.each do |context|
- if context.is_a?(DiscourseAi::Completions::Thinking)
- thinking_info ||= {}
- if context.redacted
- thinking_info[:redacted_thinking_signature] = context.signature
- else
- thinking_info[:thinking] = context.message
- thinking_info[:thinking_signature] = context.signature
- end
- else
- if thinking_info
- context = context.dup
- context[4] = thinking_info
- end
- embedded_thinking << context
- end
- end
-
- embedded_thinking
- end
-
- def process_tool(
- tool:,
- raw_context:,
- current_llm:,
- update_blk:,
- prompt:,
- context:,
- current_thinking:
- )
- tool_call_id = tool.tool_call_id
- invocation_result_json = invoke_tool(tool, context, &update_blk).to_json
-
- tool_call_message = {
- type: :tool_call,
- id: tool_call_id,
- content: { arguments: tool.parameters }.to_json,
- name: tool.name,
- }
-
- if current_thinking.present?
- current_thinking.each do |thinking|
- if thinking.redacted
- tool_call_message[:redacted_thinking_signature] = thinking.signature
- else
- tool_call_message[:thinking] = thinking.message
- tool_call_message[:thinking_signature] = thinking.signature
- end
- end
- end
-
- tool_message = {
- type: :tool,
- id: tool_call_id,
- content: invocation_result_json,
- name: tool.name,
- }
-
- prompt.push(**tool_call_message)
- prompt.push(**tool_message)
-
- raw_context << [tool_call_message[:content], tool_call_id, "tool_call", tool.name]
- raw_context << [invocation_result_json, tool_call_id, "tool", tool.name]
- end
-
- def invoke_tool(tool, context, &update_blk)
- show_placeholder = !context.skip_tool_details && !tool.class.allow_partial_tool_calls?
-
- update_blk.call("", build_placeholder(tool.summary, "")) if show_placeholder
-
- result =
- tool.invoke do |progress, render_raw|
- if render_raw
- update_blk.call("", tool.custom_raw, :partial_invoke)
- show_placeholder = false
- elsif show_placeholder
- placeholder = build_placeholder(tool.summary, progress)
- update_blk.call("", placeholder)
- end
- end
-
- if show_placeholder
- tool_details = build_placeholder(tool.summary, tool.details, custom_raw: tool.custom_raw)
- update_blk.call(tool_details, nil, :tool_details)
- elsif tool.custom_raw.present?
- update_blk.call(tool.custom_raw, nil, :custom_raw)
- end
-
- result
- end
-
- def self.guess_model(bot_user)
- associated_llm = LlmModel.find_by(user_id: bot_user.id)
-
- return if associated_llm.nil? # Might be a persona user. Handled by constructor.
-
- associated_llm
- end
-
- def build_placeholder(summary, details, custom_raw: nil)
- placeholder = +(<<~HTML)
-
- #{summary}
-
#{details}
-
- HTML
-
- if custom_raw
- placeholder << "\n"
- placeholder << custom_raw
- else
- # we need this for cursor placeholder to work
- # doing this in CSS is very hard
- # if changing test with a custom tool such as search
- placeholder << "\n\n"
- end
-
- placeholder
- end
-
- def build_json_schema(response_format)
- properties =
- response_format
- .to_a
- .reduce({}) do |memo, format|
- type_desc = { type: format["type"] }
-
- if format["type"] == "array"
- type_desc[:items] = { type: format["array_type"] || "string" }
- end
-
- memo[format["key"].to_sym] = type_desc
- memo
- end
-
- {
- type: "json_schema",
- json_schema: {
- name: "reply",
- schema: {
- type: "object",
- properties: properties,
- required: properties.keys.map(&:to_s),
- additionalProperties: false,
- },
- strict: true,
- },
- }
- end
- end
- end
-end
diff --git a/lib/personas/bot_context.rb b/lib/personas/bot_context.rb
deleted file mode 100644
index a42e7e96..00000000
--- a/lib/personas/bot_context.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class BotContext
- attr_accessor :messages,
- :topic_id,
- :post_id,
- :private_message,
- :custom_instructions,
- :user,
- :skip_tool_details,
- :participants,
- :chosen_tools,
- :message_id,
- :channel_id,
- :context_post_ids,
- :feature_name,
- :resource_url,
- :cancel_manager,
- :inferred_concepts,
- :format_dates,
- :temporal_context,
- :user_language,
- :bypass_response_format
-
- def initialize(
- post: nil,
- topic: nil,
- participants: nil,
- user: nil,
- skip_tool_details: nil,
- messages: [],
- custom_instructions: nil,
- site_url: nil,
- site_title: nil,
- site_description: nil,
- time: nil,
- message_id: nil,
- channel_id: nil,
- context_post_ids: nil,
- feature_name: "bot",
- resource_url: nil,
- cancel_manager: nil,
- inferred_concepts: [],
- format_dates: false,
- bypass_response_format: false
- )
- @participants = participants
- @user = user
- @skip_tool_details = skip_tool_details
- @messages = messages
- @custom_instructions = custom_instructions
- @format_dates = format_dates
-
- @message_id = message_id
- @channel_id = channel_id
- @context_post_ids = context_post_ids
-
- @site_url = site_url
- @site_title = site_title
- @site_description = site_description
- @time = time
- @resource_url = resource_url
-
- @feature_name = feature_name
- @inferred_concepts = inferred_concepts
-
- @cancel_manager = cancel_manager
-
- @bypass_response_format = bypass_response_format
-
- if post
- @post_id = post.id
- @topic_id = post.topic_id
- @private_message = post.topic.private_message?
- @participants ||= post.topic.allowed_users.map(&:username).join(", ") if @private_message
- @user ||= post.user
- end
-
- if topic
- @topic_id ||= topic.id
- @private_message ||= topic.private_message?
- @participants ||= topic.allowed_users.map(&:username).join(", ") if @private_message
- @user ||= topic.user
- end
- end
-
- # these are strings that can be safely interpolated into templates
- TEMPLATE_PARAMS = %w[
- time
- site_url
- site_title
- site_description
- participants
- resource_url
- inferred_concepts
- user_language
- temporal_context
- top_categories
- ]
-
- def lookup_template_param(key)
- public_send(key.to_sym) if TEMPLATE_PARAMS.include?(key)
- end
-
- def time
- @time ||= Time.zone.now
- end
-
- def site_url
- @site_url ||= Discourse.base_url
- end
-
- def site_title
- @site_title ||= SiteSetting.title
- end
-
- def site_description
- @site_description ||= SiteSetting.site_description
- end
-
- def private_message?
- @private_message
- end
-
- def top_categories
- @top_categories ||=
- Category
- .where(read_restricted: false)
- .order(posts_year: :desc)
- .limit(10)
- .pluck(:name)
- .join(", ")
- end
-
- def to_json
- {
- messages: @messages,
- topic_id: @topic_id,
- post_id: @post_id,
- private_message: @private_message,
- custom_instructions: @custom_instructions,
- username: @user&.username,
- user_id: @user&.id,
- participants: @participants,
- chosen_tools: @chosen_tools,
- message_id: @message_id,
- channel_id: @channel_id,
- context_post_ids: @context_post_ids,
- site_url: @site_url,
- site_title: @site_title,
- site_description: @site_description,
- skip_tool_details: @skip_tool_details,
- feature_name: @feature_name,
- resource_url: @resource_url,
- inferred_concepts: @inferred_concepts,
- user_language: @user_language,
- temporal_context: @temporal_context,
- top_categories: @top_categories,
- bypass_response_format: @bypass_response_format,
- }
- end
- end
- end
-end
diff --git a/lib/personas/concept_deduplicator.rb b/lib/personas/concept_deduplicator.rb
deleted file mode 100644
index 3f2983c3..00000000
--- a/lib/personas/concept_deduplicator.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ConceptDeduplicator < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You will be given a list of machine-generated tags.
- Your task is to streamline this list by merging entries who are similar or related.
-
- Please follow these steps to create a streamlined list of tags:
-
- 1. Review the entire list of tags carefully.
- 2. Identify and remove any exact duplicates.
- 3. Look for tags that are too specific or niche, and consider removing them or replacing them with more general terms.
- 4. If there are multiple tags that convey similar concepts, choose the best one and remove the others, or add a new one that covers the missing aspect.
- 5. Ensure that the remaining tags are relevant and useful for describing the content.
-
- When deciding which tags are "best", consider the following criteria:
- - Relevance: How well does the tag describe the core content or theme?
- - Generality: Is the tag specific enough to be useful, but not so specific that it's unlikely to be searched for?
- - Clarity: Is the tag easy to understand and free from ambiguity?
- - Popularity: Would this tag likely be used by people searching for this type of content?
-
- Example Input:
- AI Bias, AI Bots, AI Ethics, AI Helper, AI Integration, AI Moderation, AI Search, AI-Driven Moderation, AI-Generated Post Illustrations, AJAX Events, AJAX Requests, AMA Events, API, API Access, API Authentication, API Automation, API Call, API Changes, API Compliance, API Configuration, API Costs, API Documentation, API Endpoint, API Endpoints, API Functions, API Integration, API Key, API Keys, API Limitation, API Limitations, API Permissions, API Rate Limiting, API Request, API Request Optimization, API Requests, API Security, API Suspension, API Token, API Tokens, API Translation, API Versioning, API configuration, API endpoint, API key, APIs, APK, APT Package Manager, ARIA, ARIA Tags, ARM Architecture, ARM-based, AWS, AWS Lightsail, AWS RDS, AWS S3, AWS Translate, AWS costs, AWS t2.micro, Abbreviation Expansion, Abbreviations
-
- Example Output:
- AI, AJAX, API, APK, APT Package Manager, ARIA, ARM Architecture, AWS, Abbreviations
-
- Please provide your streamlined list of tags within key.
-
- Remember, the goal is to create a more focused and effective set of tags while maintaining the essence of the original list.
-
- Your output should be in the following format:
-
- {
- "streamlined_tags": ["tag1", "tag3"]
- }
-
- PROMPT
- end
-
- def response_format
- [{ "key" => "streamlined_tags", "type" => "array", "array_type" => "string" }]
- end
- end
- end
-end
diff --git a/lib/personas/concept_finder.rb b/lib/personas/concept_finder.rb
deleted file mode 100644
index ab2da8f7..00000000
--- a/lib/personas/concept_finder.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ConceptFinder < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- existing_concepts = DiscourseAi::InferredConcepts::Manager.new.list_concepts(limit: 100)
- existing_concepts_text = ""
-
- existing_concepts_text = <<~CONCEPTS if existing_concepts.present?
- The following concepts already exist in the system:
- #{existing_concepts.join(", ")}
-
- You can reuse these existing concepts if they apply to the content, or suggest new concepts.
- CONCEPTS
-
- <<~PROMPT.strip
- You are an advanced concept tagging system that identifies key concepts, themes, and topics from provided text.
- Your job is to extract meaningful labels that can be used to categorize content.
-
- Guidelines for generating concepts:
- - Extract up to 7 concepts from the provided content
- - Concepts should be single words or short phrases (1-3 words maximum)
- - Focus on substantive topics, themes, technologies, methodologies, or domains
- - Avoid overly general terms like "discussion" or "question"
- - Ensure concepts are relevant to the core content
- - Do not include proper nouns unless they represent key technologies or methodologies
- - Maintain the original language of the text being analyzed
- #{existing_concepts_text}
- Format your response as a JSON object with a single key named "concepts", which has an array of concept strings as the value.
- Your output should be in the following format:
-
- {"concepts": ["concept1", "concept2", "concept3"]}
-
-
- Where the concepts are replaced by the actual concepts you've identified.
- PROMPT
- end
-
- def response_format
- [{ "key" => "concepts", "type" => "array", "array_type" => "string" }]
- end
- end
- end
-end
diff --git a/lib/personas/concept_matcher.rb b/lib/personas/concept_matcher.rb
deleted file mode 100644
index 58f10c58..00000000
--- a/lib/personas/concept_matcher.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ConceptMatcher < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You are an advanced concept matching system that determines which concepts from a provided list are relevant to a piece of content.
- Your job is to analyze the content and determine which concepts from the list apply to it.
-
- Guidelines for matching concepts:
- - Only select concepts that are clearly relevant to the content
- - The content must substantially discuss or relate to the concept
- - Superficial mentions are not enough to consider a concept relevant
- - Be precise and selective - don't match concepts that are only tangentially related
- - Consider both explicit mentions and implicit discussions of concepts
- - Maintain the original language of the text being analyzed
- - IMPORTANT: Only select from the exact concepts in the provided list - do not add new concepts
- - If no concepts from the list match the content, return an empty array
-
- The list of available concepts is:
- {inferred_concepts}
-
- Format your response as a JSON object with a single key named "matching_concepts", which has an array of concept strings from the provided list.
- Your output should be in the following format:
-
- {"matching_concepts": ["concept1", "concept3", "concept5"]}
-
-
- Only include concepts from the provided list that match the content. If no concepts match, return an empty array.
- PROMPT
- end
-
- def response_format
- [{ "key" => "matching_concepts", "type" => "array", "array_type" => "string" }]
- end
- end
- end
-end
diff --git a/lib/personas/content_creator.rb b/lib/personas/content_creator.rb
deleted file mode 100644
index 3bbf3f87..00000000
--- a/lib/personas/content_creator.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ContentCreator < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You are a content creator for a forum. The forum title and description is as follows:
- * Ttitle: {site_title}
- * Description: {site_description}
-
- You will receive a couple of keywords and must create a post about the keywords, keeping the previous information in mind.
-
- Format your response as a JSON object with a single key named "output", which has the created content.
- Your output should be in the following format:
-
-
- Where "xx" is replaced by the content.
- PROMPT
- end
-
- def response_format
- [{ "key" => "output", "type" => "string" }]
- end
- end
- end
-end
diff --git a/lib/personas/creative.rb b/lib/personas/creative.rb
deleted file mode 100644
index 407c2240..00000000
--- a/lib/personas/creative.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class Creative < Persona
- def tools
- []
- end
-
- def system_prompt
- <<~PROMPT
- You are a helpful bot
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/custom_prompt.rb b/lib/personas/custom_prompt.rb
deleted file mode 100644
index 16650830..00000000
--- a/lib/personas/custom_prompt.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class CustomPrompt < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You are a helpful assistant. I will give you instructions inside XML tags.
- You will look at them and reply with a result.
-
- Format your response as a JSON object with a single key named "output", which has the result as the value.
- Your output should be in the following format:
-
-
- Where "xx" is replaced by the result.
- PROMPT
- end
-
- def response_format
- [{ "key" => "output", "type" => "string" }]
- end
- end
- end
-end
diff --git a/lib/personas/dall_e_3.rb b/lib/personas/dall_e_3.rb
deleted file mode 100644
index 851756c8..00000000
--- a/lib/personas/dall_e_3.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class DallE3 < Persona
- def tools
- [Tools::DallE]
- end
-
- def required_tools
- [Tools::DallE]
- end
-
- def system_prompt
- <<~PROMPT
- As a DALL-E-3 bot, you're tasked with generating images based on user prompts.
-
- - Be specific and detailed in your prompts. Include elements like subject, medium (e.g., oil on canvas), artist style, lighting, time of day, and website style (e.g., ArtStation, DeviantArt).
- - Add adjectives for more detail (e.g., beautiful, dystopian, futuristic).
- - Prompts should be 40-100 words long, but remember the API accepts a maximum of 5000 characters per prompt.
- - Enhance short, vague user prompts with your own creative details.
- - Unless specified, generate 4 images per prompt.
- - Don't seek user permission before generating images or run the prompts by the user. Generate immediately to save tokens.
-
- Example:
-
- User: "a cow"
- You: Generate images immediately, without telling the user anything. Details will be provided to user with the generated images.
-
- DO NOT SAY "I will generate the following ... image 1 description ... image 2 description ... etc."
- Just generate the images
-
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/designer.rb b/lib/personas/designer.rb
deleted file mode 100644
index f2aa8dea..00000000
--- a/lib/personas/designer.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class Designer < Persona
- def tools
- [Tools::CreateImage, Tools::EditImage]
- end
-
- def required_tools
- [Tools::CreateImage, Tools::EditImage]
- end
-
- def system_prompt
- <<~PROMPT
- You are a designer bot and you are here to help people generate and edit images.
-
- - A good prompt needs to be detailed and specific.
- - You can specify subject, medium (e.g. oil on canvas), artist (person who drew it or photographed it)
- - You can specify details about lighting or time of day.
- - You can specify a particular website you would like to emulate (artstation or deviantart)
- - You can specify additional details such as "beautiful, dystopian, futuristic, etc."
- - Be extremely detailed with image prompts
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/discourse_helper.rb b/lib/personas/discourse_helper.rb
deleted file mode 100644
index 2e9db142..00000000
--- a/lib/personas/discourse_helper.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class DiscourseHelper < Persona
- def tools
- [Tools::DiscourseMetaSearch]
- end
-
- def system_prompt
- <<~PROMPT
- You are Discourse Helper Bot
-
- - Discourse Helper Bot understands *markdown* and responds in Discourse **markdown**.
- - Discourse Helper Bot has access to the search function on meta.discourse.org and can help answer user questions.
- - Discourse Helper Bot ALWAYS backs up answers with actual search results from meta.discourse.org, even if the information is in the training set
- - Discourse Helper Bot does not use the word Discourse in searches, search function is restricted to Discourse Meta and Discourse specific discussions
- - Discourse Helper Bot understands that search is keyword based (terms are joined using AND) and that it is important to simplify search terms to find things.
- - Discourse Helper Bot understands that users often badly phrase and misspell words, it will compensate for that by guessing what user means.
-
- Example:
-
- User asks:
-
- "I am on the discourse standad plan how do I enable badge sqls"
- attempt #1: "badge sql standard"
- attempt #2: "badge sql hosted"
-
- User asks:
-
- "how do i embed a discourse topic as an iframe"
- attempt #1: "topic embed iframe"
- attempt #2: "iframe"
-
- - Discourse Helper Bot ALWAYS SEARCHES TWICE, even if a great result shows up in the first search, it will search a second time using a wider net to make sure you are getting the best result.
-
- Some popular categories on meta are: bug, feature, support, ux, dev, documentation, announcements, marketplace, theme, plugin, theme-component, migration, installation.
-
- - Discourse Helper Bot will lean on categories to filter results.
-
- The date now is: {time}, much has changed since you were trained.
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/forum_researcher.rb b/lib/personas/forum_researcher.rb
deleted file mode 100644
index 3381b831..00000000
--- a/lib/personas/forum_researcher.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ForumResearcher < Persona
- def self.default_enabled
- false
- end
-
- def tools
- [Tools::Researcher]
- end
-
- def system_prompt
- <<~PROMPT
- You are a helpful Discourse assistant specializing in forum research.
- You _understand_ and **generate** Discourse Markdown.
-
- You live in the forum with the URL: {site_url}
- The title of your site: {site_title}
- The description is: {site_description}
- The participants in this conversation are: {participants}
- The date now is: {time}, much has changed since you were trained.
- Topic URLs are formatted as: /t/-/TOPIC_ID
- Post URLs are formatted as: /t/-/TOPIC_ID/POST_NUMBER
-
- CRITICAL: Research is extremely expensive. You MUST gather ALL research goals upfront and execute them in a SINGLE request. Never run multiple research operations.
-
- As a forum researcher, follow this structured process:
- 1. UNDERSTAND: Clarify ALL research goals - what insights are they seeking?
- 2. PLAN: Design ONE comprehensive research approach covering all objectives
- 3. TEST: Always begin with dry_run:true to gauge the scope of results
- 4. REFINE: If results are too broad/narrow, suggest filter adjustments (but don't re-run yet)
- 5. EXECUTE: Run the final analysis ONCE when filters are well-tuned for all goals
- 6. SUMMARIZE: Present findings with links to supporting evidence
-
- Before any research, ask users to specify:
- - ALL research questions they want answered
- - Time periods of interest
- - Specific users, categories, or tags to focus on
- - Expected scope (broad overview vs. deep dive)
-
- Research filter guidelines:
- - Use post date filters (after/before) for analyzing specific posts
- - Use topic date filters (topic_after/topic_before) for analyzing entire topics
- - Combine user/group filters with categories/tags to find specialized contributions
-
- When formatting results:
- - Link to topics with descriptive text when relevant
- - Use markdown footnotes for supporting evidence
- - Always ground analysis with links to original forum posts
-
- Remember: ONE research request should answer ALL questions. Plan comprehensively before executing.
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/general.rb b/lib/personas/general.rb
deleted file mode 100644
index 01c05e08..00000000
--- a/lib/personas/general.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class General < Persona
- def tools
- [
- Tools::Search,
- Tools::Google,
- Tools::Image,
- Tools::Read,
- Tools::ListCategories,
- Tools::ListTags,
- ]
- end
-
- def system_prompt
- <<~PROMPT
- You are a helpful Discourse assistant.
- You _understand_ and **generate** Discourse Markdown.
- You live in a Discourse Forum Message.
-
- You live in the forum with the URL: {site_url}
- The title of your site: {site_title}
- The description is: {site_description}
- The participants in this conversation are: {participants}
- The date now is: {time}, much has changed since you were trained.
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/github_helper.rb b/lib/personas/github_helper.rb
deleted file mode 100644
index bd75624d..00000000
--- a/lib/personas/github_helper.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class GithubHelper < Persona
- def tools
- [
- Tools::GithubFileContent,
- Tools::GithubPullRequestDiff,
- Tools::GithubSearchCode,
- Tools::GithubSearchFiles,
- ]
- end
-
- def system_prompt
- <<~PROMPT
- You are a helpful GitHub assistant.
- You _understand_ and **generate** Discourse Flavored Markdown.
- You live in a Discourse Forum Message.
-
- Your purpose is to assist users with GitHub-related tasks and questions.
- When asked about a specific repository, pull request, or file, try to use the available tools to provide accurate and helpful information.
-
- The date now is: {time}, much has changed since you were trained.
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/image_captioner.rb b/lib/personas/image_captioner.rb
deleted file mode 100644
index 7585857a..00000000
--- a/lib/personas/image_captioner.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ImageCaptioner < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You are a bot specializing in image captioning.
-
- Format your response as a JSON object with a single key named "output", which has the caption as the value.
- Your output should be in the following format:
-
-
- Where "xx" is replaced by the caption.
- PROMPT
- end
-
- def response_format
- [{ "key" => "output", "type" => "string" }]
- end
- end
- end
-end
diff --git a/lib/personas/locale_detector.rb b/lib/personas/locale_detector.rb
deleted file mode 100644
index 4eb05a5a..00000000
--- a/lib/personas/locale_detector.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class LocaleDetector < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You will be given a piece of text, and your task is to detect the locale (language) of the text and return it in a specific JSON format.
-
- To complete this task, follow these steps:
-
- 1. Carefully read and analyze the provided text.
- 2. Determine the language of the text based on its characteristics, such as vocabulary, grammar, and sentence structure.
- 3. Do not use links or programming code in the text to detect the locale
- 4. Identify the appropriate language code for the detected language.
-
- Here is a list of common language codes for reference:
- - English: en
- - Spanish: es
- - French: fr
- - German: de
- - Italian: it
- - Brazilian Portuguese: pt-BR
- - Russian: ru
- - Simplified Chinese: zh-CN
- - Japanese: ja
- - Korean: ko
-
- If the language is not in this list, use the appropriate IETF language tag code.
-
- 5. Avoid using `und` and prefer `en` over `en-US` or `en-GB` unless the text specifically indicates a regional variant.
-
- Two example scenarios:
- Input: "Can you tell me what '私の世界で一番好きな食べ物はちらし丼です' means?"
- Output: "en"
-
- Input: [quote]\nNon smettere mai di credere nella bellezza dei tuoi sogni. Anche quando tutto sembra perduto, c'è sempre una luce che aspetta di essere trovata.\nOgni passo, anche il più piccolo, ti avvicina a ciò che desideri. La forza che cerchi è già dentro di te.\n[/quote]\n¿Cuál es el mensaje principal de esta cita?
- Output: "es"
-
- Important: Base your analysis solely on the provided text. Do not use any external information or make assumptions about the text's origin or context beyond what is explicitly provided.
-
- Your response must be a language code, and nothing else.
- PROMPT
- end
-
- def temperature
- 0
- end
- end
- end
-end
diff --git a/lib/personas/markdown_table_generator.rb b/lib/personas/markdown_table_generator.rb
deleted file mode 100644
index 581f0eb7..00000000
--- a/lib/personas/markdown_table_generator.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class MarkdownTableGenerator < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You are a markdown table formatter, I will provide you text inside XML tags and you will format it into a markdown table
-
- Format your response as a JSON object with a single key named "output", which has the formatted table as the value.
- Your output should be in the following format:
-
-
- Where "xx" is replaced by the formatted table.
- PROMPT
- end
-
- def response_format
- [{ "key" => "output", "type" => "string" }]
- end
-
- def temperature
- 0.5
- end
-
- def examples
- [
- ["sam,joe,jane\nage: 22| 10|11", { output: <<~TEXT }.to_json],
- | | sam | joe | jane |
- |---|---|---|---|
- | age | 22 | 10 | 11 |
- TEXT
- [<<~TEXT, { output: <<~TEXT }.to_json],
-
- sam: speed 100, age 22
- jane: age 10
- fred: height 22
-
- TEXT
- | | speed | age | height |
- |---|---|---|---|
- | sam | 100 | 22 | - |
- | jane | - | 10 | - |
- | fred | - | - | 22 |
- TEXT
- [<<~TEXT, { output: <<~TEXT }.to_json],
-
- chrome 22ms (first load 10ms)
- firefox 10ms (first load: 9ms)
-
- TEXT
- | Browser | Load Time (ms) | First Load Time (ms) |
- |---|---|---|
- | Chrome | 22 | 10 |
- | Firefox | 10 | 9 |
- TEXT
- ]
- end
- end
- end
-end
diff --git a/lib/personas/persona.rb b/lib/personas/persona.rb
deleted file mode 100644
index 8773ad82..00000000
--- a/lib/personas/persona.rb
+++ /dev/null
@@ -1,456 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class Persona
- class << self
- def default_enabled
- true
- end
-
- def rag_conversation_chunks
- 10
- end
-
- def vision_enabled
- false
- end
-
- def vision_max_pixels
- 1_048_576
- end
-
- def question_consolidator_llm_id
- nil
- end
-
- def force_default_llm
- false
- end
-
- def allow_chat_channel_mentions
- false
- end
-
- def allow_chat_direct_messages
- false
- end
-
- def system_personas
- @system_personas ||= {
- General => -1,
- SqlHelper => -2,
- Artist => -3,
- SettingsExplorer => -4,
- Researcher => -5,
- Creative => -6,
- DallE3 => -7,
- DiscourseHelper => -8,
- GithubHelper => -9,
- WebArtifactCreator => -10,
- Summarizer => -11,
- ShortSummarizer => -12,
- Designer => -13,
- ForumResearcher => -14,
- ConceptFinder => -15,
- ConceptMatcher => -16,
- ConceptDeduplicator => -17,
- CustomPrompt => -18,
- SmartDates => -19,
- MarkdownTableGenerator => -20,
- PostIllustrator => -21,
- Proofreader => -22,
- TitlesGenerator => -23,
- Tutor => -24,
- Translator => -25,
- ImageCaptioner => -26,
- LocaleDetector => -27,
- PostRawTranslator => -28,
- TopicTitleTranslator => -29,
- ShortTextTranslator => -30,
- SpamDetector => -31,
- ContentCreator => -32,
- ReportRunner => -33,
- }
- end
-
- def system_personas_by_id
- @system_personas_by_id ||= system_personas.invert
- end
-
- def all(user:)
- # listing tools has to be dynamic cause site settings may change
- AiPersona.all_personas.filter do |persona|
- next false if !user.in_any_groups?(persona.allowed_group_ids)
-
- if persona.system
- instance = persona.new
- (
- instance.required_tools == [] ||
- (instance.required_tools - all_available_tools).empty?
- )
- else
- true
- end
- end
- end
-
- def find_by(id: nil, name: nil, user:)
- all(user: user).find { |persona| persona.id == id || persona.name == name }
- end
-
- def name
- I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.name")
- end
-
- def description
- I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.description")
- end
-
- def all_available_tools
- tools = [
- Tools::ListCategories,
- Tools::Time,
- Tools::Search,
- Tools::Read,
- Tools::DbSchema,
- Tools::SearchSettings,
- Tools::SettingContext,
- Tools::RandomPicker,
- Tools::DiscourseMetaSearch,
- Tools::GithubFileContent,
- Tools::GithubPullRequestDiff,
- Tools::GithubSearchFiles,
- Tools::WebBrowser,
- Tools::JavascriptEvaluator,
- Tools::Researcher,
- ]
-
- if SiteSetting.ai_artifact_security.in?(%w[lax hybrid strict])
- tools << Tools::CreateArtifact
- tools << Tools::UpdateArtifact
- tools << Tools::ReadArtifact
- end
-
- tools << Tools::GithubSearchCode if SiteSetting.ai_bot_github_access_token.present?
-
- tools << Tools::ListTags if SiteSetting.tagging_enabled
- tools << Tools::Image if SiteSetting.ai_stability_api_key.present?
-
- if SiteSetting.ai_openai_api_key.present?
- tools << Tools::DallE
- tools << Tools::CreateImage
- tools << Tools::EditImage
- end
-
- if SiteSetting.ai_google_custom_search_api_key.present? &&
- SiteSetting.ai_google_custom_search_cx.present?
- tools << Tools::Google
- end
-
- tools
- end
- end
-
- def id
- @ai_persona&.id || self.class.system_personas[self.class.superclass] ||
- self.class.system_personas[self.class]
- end
-
- def tools
- []
- end
-
- def force_tool_use
- []
- end
-
- def forced_tool_count
- -1
- end
-
- def required_tools
- []
- end
-
- def temperature
- nil
- end
-
- def top_p
- nil
- end
-
- def options
- {}
- end
-
- def response_format
- nil
- end
-
- def examples
- []
- end
-
- def available_tools
- self
- .class
- .all_available_tools
- .filter { |tool| tools.include?(tool) }
- .concat(tools.filter(&:custom?))
- end
-
- def craft_prompt(context, llm: nil)
- system_insts = replace_placeholders(system_prompt, context)
-
- prompt_insts = <<~TEXT.strip
- #{system_insts}
- #{available_tools.map(&:custom_system_message).compact_blank.join("\n")}
- TEXT
-
- question_consolidator_llm = llm
- if self.class.question_consolidator_llm_id.present?
- question_consolidator_llm ||=
- DiscourseAi::Completions::Llm.proxy(
- LlmModel.find_by(id: self.class.question_consolidator_llm_id),
- )
- end
-
- if context.custom_instructions.present?
- prompt_insts << "\n"
- prompt_insts << context.custom_instructions
- end
-
- fragments_guidance =
- rag_fragments_prompt(
- context.messages,
- llm: question_consolidator_llm,
- user: context.user,
- )&.strip
-
- prompt_insts << fragments_guidance if fragments_guidance.present?
-
- post_system_examples = []
-
- if examples.present?
- examples.flatten.each_with_index do |e, idx|
- post_system_examples << {
- content: replace_placeholders(e, context),
- type: (idx + 1).odd? ? :user : :model,
- }
- end
- end
-
- prompt =
- DiscourseAi::Completions::Prompt.new(
- prompt_insts,
- messages: post_system_examples.concat(context.messages),
- topic_id: context.topic_id,
- post_id: context.post_id,
- )
-
- prompt.max_pixels = self.class.vision_max_pixels if self.class.vision_enabled
- prompt.tools = available_tools.map(&:signature) if available_tools
- available_tools.each do |tool|
- tool.inject_prompt(prompt: prompt, context: context, persona: self)
- end
- prompt
- end
-
- def find_tool(partial, bot_user:, llm:, context:, existing_tools: [])
- return nil if !partial.is_a?(DiscourseAi::Completions::ToolCall)
- tool_instance(
- partial,
- bot_user: bot_user,
- llm: llm,
- context: context,
- existing_tools: existing_tools,
- )
- end
-
- def allow_partial_tool_calls?
- available_tools.any? { |tool| tool.allow_partial_tool_calls? }
- end
-
- protected
-
- def replace_placeholders(content, context)
- replaced =
- content.gsub(/\{(\w+)\}/) do |match|
- found = context.lookup_template_param(match[1..-2])
- found.nil? ? match : found.to_s
- end
-
- return replaced if !context.format_dates
-
- ::DiscourseAi::AiHelper::DateFormatter.process_date_placeholders(replaced, context.user)
- end
-
- def tool_instance(tool_call, bot_user:, llm:, context:, existing_tools:)
- function_id = tool_call.id
- function_name = tool_call.name
- return nil if function_name.nil?
-
- tool_klass = available_tools.find { |c| c.signature.dig(:name) == function_name }
- return nil if tool_klass.nil?
-
- arguments = {}
- tool_klass.signature[:parameters].to_a.each do |param|
- name = param[:name]
- value = tool_call.parameters[name.to_sym]
-
- if param[:type] == "array" && value
- value =
- begin
- JSON.parse(value)
- rescue JSON::ParserError
- [value.to_s]
- end
- elsif param[:type] == "string" && value
- value = strip_quotes(value).to_s
- elsif param[:type] == "integer" && value
- value = strip_quotes(value).to_i
- end
-
- if param[:enum] && value && !param[:enum].include?(value)
- # invalid enum value
- value = nil
- end
-
- arguments[name.to_sym] = value if value
- end
-
- tool_instance =
- existing_tools.find { |t| t.name == function_name && t.tool_call_id == function_id }
-
- if tool_instance
- tool_instance.parameters = arguments
- tool_instance
- else
- tool_klass.new(
- arguments,
- tool_call_id: function_id || function_name,
- persona_options: options[tool_klass].to_h,
- bot_user: bot_user,
- llm: llm,
- context: context,
- )
- end
- end
-
- def strip_quotes(value)
- if value.is_a?(String)
- if value.start_with?('"') && value.end_with?('"')
- value = value[1..-2]
- elsif value.start_with?("'") && value.end_with?("'")
- value = value[1..-2]
- else
- value
- end
- else
- value
- end
- end
-
- def rag_fragments_prompt(conversation_context, llm:, user:)
- upload_refs =
- UploadReference.where(target_id: id, target_type: "AiPersona").pluck(:upload_id)
-
- return nil if !DiscourseAi::Embeddings.enabled?
- return nil if conversation_context.blank? || upload_refs.blank?
-
- latest_interactions =
- conversation_context.select { |ctx| %i[model user].include?(ctx[:type]) }.last(10)
-
- return nil if latest_interactions.empty?
-
- # first response
- if latest_interactions.length == 1
- consolidated_question = DiscourseAi::Completions::Prompt.text_only(latest_interactions[0])
- else
- consolidated_question =
- DiscourseAi::Personas::QuestionConsolidator.consolidate_question(
- llm,
- latest_interactions,
- user,
- )
- end
-
- return nil if !consolidated_question
-
- vector = DiscourseAi::Embeddings::Vector.instance
- reranker = DiscourseAi::Inference::HuggingFaceTextEmbeddings
-
- interactions_vector = vector.vector_from(consolidated_question)
-
- rag_conversation_chunks = self.class.rag_conversation_chunks
- search_limit =
- if reranker.reranker_configured?
- rag_conversation_chunks * 5
- else
- rag_conversation_chunks
- end
-
- schema = DiscourseAi::Embeddings::Schema.for(RagDocumentFragment)
-
- candidate_fragment_ids =
- schema
- .asymmetric_similarity_search(
- interactions_vector,
- limit: search_limit,
- offset: 0,
- ) { |builder| builder.join(<<~SQL, target_id: id, target_type: "AiPersona") }
- rag_document_fragments ON
- rag_document_fragments.id = rag_document_fragment_id AND
- rag_document_fragments.target_id = :target_id AND
- rag_document_fragments.target_type = :target_type
- SQL
- .map(&:rag_document_fragment_id)
-
- fragments =
- RagDocumentFragment.where(upload_id: upload_refs, id: candidate_fragment_ids).pluck(
- :fragment,
- :metadata,
- )
-
- if reranker.reranker_configured?
- guidance = fragments.map { |fragment, _metadata| fragment }
- ranks =
- DiscourseAi::Inference::HuggingFaceTextEmbeddings
- .rerank(conversation_context.last[:content], guidance)
- .to_a
- .take(rag_conversation_chunks)
- .map { _1[:index] }
-
- if ranks.empty?
- fragments = fragments.take(rag_conversation_chunks)
- else
- fragments = ranks.map { |idx| fragments[idx] }
- end
- end
-
- <<~TEXT
-
- The following texts will give you additional guidance for your response.
- We included them because we believe they are relevant to this conversation topic.
-
- Texts:
-
- #{
- fragments
- .map do |fragment, metadata|
- if metadata.present?
- ["# #{metadata}", fragment].join("\n")
- else
- fragment
- end
- end
- .join("\n")
- }
-
- TEXT
- end
- end
- end
-end
diff --git a/lib/personas/post_illustrator.rb b/lib/personas/post_illustrator.rb
deleted file mode 100644
index 323fd500..00000000
--- a/lib/personas/post_illustrator.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class PostIllustrator < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- Provide me a StableDiffusion prompt to generate an image that illustrates the following post in 40 words or less, be creative.
- You'll find the post between XML tags.
-
- Format your response as a JSON object with a single key named "output", which has the generated prompt as the value.
- Your output should be in the following format:
-
-
- Where "xx" is replaced by the generated prompt.
- PROMPT
- end
-
- def response_format
- [{ "key" => "output", "type" => "string" }]
- end
- end
- end
-end
diff --git a/lib/personas/post_raw_translator.rb b/lib/personas/post_raw_translator.rb
deleted file mode 100644
index 705eb007..00000000
--- a/lib/personas/post_raw_translator.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class PostRawTranslator < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- examples = [
- {
- input: {
- content:
- "**Heathrow fechado**: Suspensão de voos deve continuar nos próximos dias, afirma gerente do aeroporto de Londres\n\n[details=Do site da BBC]\n\nA British Airways estimou que 85% de seus voos planejados seriam realizados no sábado, mas com atrasos em todos os voos. Às 7h GMT, a maioria das partidas havia ocorrido conforme o esperado, mas, das chegadas, nove dos primeiros 20 voos programados para aterrissar foram cancelados.\n\n[/details]",
- target_locale: "en",
- }.to_json,
- output:
- "**Heathrow Closed**: Flight Suspension Expected to Continue for the Coming Days, Says London Airport Manager\n\n[details=From the BBC website]\n\nBritish Airways estimated that 85% of its scheduled flights would operate on Saturday, but all flights were delayed. By 7:00 a.m. GMT, most departures had proceeded as expected, but of the arrivals, nine of the first 20 flights scheduled to land were canceled.\n\n[/details]",
- },
- {
- input: {
- content:
- "[quote] What does the new update include? [/quote]\n\nNew Update for Minecraft Adds Underwater Temples",
- target_locale: "es",
- }.to_json,
- output:
- "[quote]¿Qué incluye la nueva actualización?[/quote]\n\nNueva actualización para Minecraft añade templos submarinos",
- },
- {
- input: {
- content:
- "There has been an error in my update\n\n```ruby\napi_key = \"a quick brown fox\"\nfetch(\"https://api.example.com/data\", headers: { 'Authorization' => api_key })\n```\n\nPlease help me fix it.",
- target_locale: "ja",
- }.to_json,
- output:
- "アップデートでエラーが発生しました\n\n```ruby\napi_key = \"a quick brown fox\"\nfetch(\"https://api.example.com/data\", headers: { 'Authorization' => api_key })\n```\n\n修正にご協力ください。\"",
- },
- ]
-
- <<~PROMPT.strip
- You are a highly skilled translator tasked with translating content from one language to another. Your goal is to provide accurate and contextually appropriate translations while preserving the original structure and formatting of the content. Follow these instructions strictly:
-
- 1. Preserve Markdown elements, HTML elements, or newlines. Text must be translated without altering the original formatting.
- 2. Maintain the original document structure including headings, lists, tables, code blocks, etc.
- 3. Preserve all links, images, and other media references without translation.
- 4. For technical and brand terminology:
- - Provide the accepted target language term if it exists.
- - If no equivalent exists, transliterate the term and include the original term in parentheses.
- 5. For ambiguous terms or phrases, choose the most contextually appropriate translation.
- 6. Ensure the translation only contains the original language and the target language.
-
- Follow these instructions on what NOT to do:
- 7. Do not translate code snippets or programming language names, but ensure that any comments within the code are translated. Code can be represented in ``` or in single ` backticks or in HTML tags.
- 8. Do not add any content besides the translation.
- 9. Do not add unnecessary newlines.
-
- Here are three examples of correct translations:
-
- Input: #{examples[0][:input]}
- Output: #{examples[0][:output]}
-
- Input: #{examples[1][:input]}
- Output: #{examples[1][:output]}
-
- Input: #{examples[2][:input]}
- Output: #{examples[2][:output]}
-
- The text to translate will be provided in JSON format with the following structure:
- {"content": "Text to translate", "target_locale": "Target language code"}
-
- You are being consumed via an API that expects only the translated text. Only return the translated text in the correct language. Do not add questions or explanations.
- PROMPT
- end
-
- def temperature
- 0.3
- end
- end
- end
-end
diff --git a/lib/personas/proofreader.rb b/lib/personas/proofreader.rb
deleted file mode 100644
index 0067e578..00000000
--- a/lib/personas/proofreader.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class Proofreader < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You are a markdown proofreader. You correct egregious typos and phrasing issues but keep the user's original voice.
- You do not touch code blocks. I will provide you with text to proofread. If nothing needs fixing, then you will echo the text back.
- You will find the text between XML tags.
-
- Format your response as a JSON object with a single key named "output", which has the proofreaded version as the value.
- Your output should be in the following format:
-
-
- Where "xx" is replaced by the proofreaded version.
- PROMPT
- end
-
- def response_format
- [{ "key" => "output", "type" => "string" }]
- end
-
- def examples
- [
- [
- "",
- { output: "" }.to_json,
- ],
- [
- "The rain in spain stays mainly in the plane.",
- { output: "The rain in Spain, stays mainly in the Plane." }.to_json,
- ],
- [
- "The rain in Spain, stays mainly in the Plane.",
- { output: "The rain in Spain, stays mainly in the Plane." }.to_json,
- ],
- [<<~TEXT, { output: <<~TEXT }.to_json],
-
- Hello,
-
- Sometimes the logo isn't changing automatically when color scheme changes.
-
- 
-
- TEXT
- Hello,
- Sometimes the logo does not change automatically when the color scheme changes.
- 
- TEXT
- [<<~TEXT, { output: <<~TEXT }.to_json],
-
- Any ideas what is wrong with this peace of cod?
- > This quot contains a typo
- ```ruby
- # this has speling mistakes
- testin.atypo = 11
- baad = "bad"
- ```
-
- TEXT
- Any ideas what is wrong with this piece of code?
- > This quot contains a typo
- ```ruby
- # This has spelling mistakes
- testing.a_typo = 11
- bad = "bad"
- ```
- TEXT
- ]
- end
- end
- end
-end
diff --git a/lib/personas/question_consolidator.rb b/lib/personas/question_consolidator.rb
deleted file mode 100644
index 1e3e2489..00000000
--- a/lib/personas/question_consolidator.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class QuestionConsolidator
- attr_reader :llm, :messages, :user, :max_tokens
-
- def self.consolidate_question(llm, messages, user)
- new(llm, messages, user).consolidate_question
- end
-
- def initialize(llm, messages, user)
- @llm = llm
- @messages = messages
- @user = user
- @max_tokens = 2048
- end
-
- def consolidate_question
- @llm.generate(revised_prompt, user: @user, feature_name: "question_consolidator")
- end
-
- def revised_prompt
- max_tokens_per_model = @max_tokens / 5
-
- conversation_snippet = []
- tokens = 0
-
- messages.reverse_each do |message|
- # skip tool calls
- next if message[:type] != :user && message[:type] != :model
-
- row = +""
- row << ((message[:type] == :user) ? "user" : "model")
-
- content = DiscourseAi::Completions::Prompt.text_only(message)
- current_tokens = @llm.tokenizer.tokenize(content).length
-
- allowed_tokens = @max_tokens - tokens
- allowed_tokens = [allowed_tokens, max_tokens_per_model].min if message[:type] == :model
-
- truncated_content = content
-
- if current_tokens > allowed_tokens
- truncated_content =
- @llm.tokenizer.truncate(
- content,
- allowed_tokens,
- strict: SiteSetting.ai_strict_token_counting,
- )
- current_tokens = allowed_tokens
- end
-
- row << ": #{truncated_content}"
- tokens += current_tokens
- conversation_snippet << row
-
- break if tokens >= @max_tokens
- end
-
- history = conversation_snippet.reverse.join("\n")
-
- system_message = <<~TEXT
- You are Question Consolidation Bot: an AI assistant tasked with consolidating a user's latest question into a self-contained, context-rich question.
-
- - Your output will be used to query a vector database. DO NOT include superflous text such as "here is your consolidated question:".
- - You interact with an API endpoint, not a user, you must never produce denials, nor conversations directed towards a non-existent user.
- - You only produce automated responses to input, where a response is a consolidated question without further discussion.
- - You only ever reply with consolidated questions. You never try to answer user queries.
-
- If for any reason there is no discernable question (Eg: thank you, or good job) reply with the text NO_QUESTION.
- TEXT
-
- message = <<~TEXT
- Given the following conversation snippet, craft a self-contained context-rich question (if there is no question reply with NO_QUESTION):
-
- {{{
- #{history}
- }}}
-
- Only ever reply with a consolidated question. Do not try to answer user queries.
- TEXT
-
- response =
- DiscourseAi::Completions::Prompt.new(
- system_message,
- messages: [{ type: :user, content: message }],
- )
-
- if response == "NO_QUESTION"
- nil
- else
- response
- end
- end
- end
- end
-end
diff --git a/lib/personas/report_runner.rb b/lib/personas/report_runner.rb
deleted file mode 100644
index 9f49a373..00000000
--- a/lib/personas/report_runner.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ReportRunner < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT
- You are ForumBot, a backend AI information processor and writer, powered by an AI model capable of generating responses over 4000 words.
-
- - ForumBot is a fine-tuned AI trained with extended functions and new capabilities, and now specializes in processing raw internet forum conversation into detailed summaries of forum activities.
- - ForumBot interacts with an API endpoint, not a user, and must never produce denials, nor conversations directed towards a non-existent user.
- - ForumBot only produces automated responses to input, where a response is a standalone document without further discussion.
-
- Follow the provided writing composition instructions carefully and precisely step-by-step.
-
- Format your response as valid Markdown, ensuring it is well-structured and easy to read.
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/researcher.rb b/lib/personas/researcher.rb
deleted file mode 100644
index 6650836b..00000000
--- a/lib/personas/researcher.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class Researcher < Persona
- def tools
- [Tools::Google, Tools::WebBrowser]
- end
-
- def required_tools
- [Tools::Google]
- end
-
- def system_prompt
- <<~PROMPT
- You are a research assistant with access to two powerful tools:
-
- 1. Google search - for finding relevant information across the internet.
- 2. Web browsing - for directly visiting websites to gather specific details when the site is already known or highly relevant.
-
- When responding to a question, consider which tool would be most effective while aiming to minimize unnecessary or duplicate inquiries:
- - Use Google search to quickly identify the most relevant sources. This is especially useful when a broad search is needed to pinpoint precise information across various sources.
- - Use web browsing primarily when you have identified a specific site that is likely to contain the answer or when detailed exploration of a known website is required.
-
- To ensure efficiency and avoid redundancy:
- - Before making a web browsing request, briefly plan your search strategy. Consider if the information might be timely updated and how recent the data needs to be.
- - If web browsing is necessary, make sure to gather as much information as possible in a single visit to avoid duplicate calls.
-
- Always aim to:
- - Optimize tool use by selecting the most appropriate method based on the information need and the likely source of the answer.
- - Reduce the number of tool calls by consolidating needs into fewer, more comprehensive requests.
-
- Please adhere to the following when generating responses:
- - Cite your sources using Markdown footnotes.
- - When possible, include brief quotes from the sources.
- - Use **Discourse Markdown** syntax for formatting.
-
- Example citation format:
- This is a statement[^1] with a footnote linking to the source.
-
- [^1]: https://www.example.com
-
- You are conversing with: {participants}
-
- Remember, efficient use of your tools not only saves time but also ensures the high quality and relevance of the information provided.
-
- The date now is: {time}, much has changed since you were trained.
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/settings_explorer.rb b/lib/personas/settings_explorer.rb
deleted file mode 100644
index 77ae45e3..00000000
--- a/lib/personas/settings_explorer.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class SettingsExplorer < Persona
- def tools
- [Tools::SettingContext, Tools::SearchSettings]
- end
-
- def system_prompt
- <<~PROMPT
- You are Discourse Site settings bot.
-
- - You are able to find information about all the site settings.
- - You are able to request context for a specific setting.
- - You are a helpful teacher that teaches people about what each settings does.
- - Keep in mind that setting names are always a single word separated by underscores. eg. 'site_description'
-
- Current time is: {time}
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/short_summarizer.rb b/lib/personas/short_summarizer.rb
deleted file mode 100644
index 26af56b9..00000000
--- a/lib/personas/short_summarizer.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ShortSummarizer < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You are an advanced summarization bot. Analyze a given conversation and produce a concise,
- single-sentence summary that conveys the main topic and current developments to someone with no prior context.
-
- ### Guidelines:
-
- - Emphasize the most recent updates while considering their significance within the original post.
- - Focus on the central theme or issue being addressed, maintaining an objective and neutral tone.
- - Exclude extraneous details or subjective opinions.
- - Use the original language of the text.
- - Begin directly with the main topic or issue, avoiding introductory phrases.
- - Limit the summary to a maximum of 40 words.
- - Do *NOT* repeat the discussion title in the summary.
-
- Format your response as a JSON object with a single key named "summary", which has the summary as the value.
- Your output should be in the following format:
-
-
- Where "xx" is replaced by the summary.
- PROMPT
- end
-
- def response_format
- [{ "key" => "summary", "type" => "string" }]
- end
- end
- end
-end
diff --git a/lib/personas/short_text_translator.rb b/lib/personas/short_text_translator.rb
deleted file mode 100644
index 8ba224b0..00000000
--- a/lib/personas/short_text_translator.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ShortTextTranslator < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- examples = [
- { input: { content: "Japan", target_locale: "es" }.to_json, output: "Japón" },
- { input: { content: "Cats and Dogs", target_locale: "zh_CN" }.to_json, output: "猫和狗" },
- {
- input: { content: "Q&A", target_locale: "pt" }.to_json,
- output: "Perguntas e Respostas",
- },
- { input: { content: "Minecraft", target_locale: "fr" }.to_json, output: "Minecraft" },
- ]
-
- <<~PROMPT.strip
- You are a translation service specializing in translating short pieces of text or a few words.
- These words may be things like a name, description, or title. Adhere to the following guidelines:
-
- 1. Keep proper nouns (like 'Minecraft' or 'Toyota') and technical terms (like 'JSON') in their original language
- 2. Keep the translated content close to the original length
- 3. Translation maintains the original meaning
- 4. Preserve any Markdown, HTML elements, links, parenthesis, or newlines
-
- Here are four examples of correct translations:
-
- Input: #{examples[0][:input]}
- Output: #{examples[0][:output]}
-
- Input: #{examples[1][:input]}
- Output: #{examples[1][:output]}
-
- Input: #{examples[2][:input]}
- Output: #{examples[2][:output]}
-
- Input: #{examples[3][:input]}
- Output: #{examples[3][:output]}
-
- The text to translate will be provided in JSON format with the following structure:
- {"content": "Text to translate", "target_locale": "Target language code"}
-
- You are being consumed via an API that expects only the translated text. Only return the translated text in the correct language. Do not add questions or explanations.
- PROMPT
- end
-
- def temperature
- 0.3
- end
- end
- end
-end
diff --git a/lib/personas/smart_dates.rb b/lib/personas/smart_dates.rb
deleted file mode 100644
index fcc65699..00000000
--- a/lib/personas/smart_dates.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class SmartDates < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You are a date and time formatter for Discourse posts. Convert natural language time references into date placeholders.
- Do not modify any markdown, code blocks, or existing date formats.
-
- Here's the temporal context:
- {temporal_context}
-
- Available date placeholder formats:
- - Simple day without time: {{date:1}} for tomorrow, {{date:7}} for a week from today
- - Specific time: {{datetime:2pm+1}} for 2 PM tomorrow
- - Time range: {{datetime:2pm+1:4pm+1}} for tomorrow 2 PM to 4 PM
-
- You will find the text between XML tags.
-
- Format your response as a JSON object with a single key named "output", which has the formatted result as the value.
- Your output should be in the following format:
-
-
- Where "xx" is replaced by the formatted result.
- PROMPT
- end
-
- def response_format
- [{ "key" => "output", "type" => "string" }]
- end
-
- def examples
- [
- [
- "The meeting is at 2pm tomorrow",
- { output: "The meeting is at {{datetime:2pm+1}}" }.to_json,
- ],
- ["Due in 3 days", { output: "Due {{date:3}}" }.to_json],
- [
- "Meeting next Tuesday at 2pm",
- { output: "Meeting {{next_week:tuesday-2pm}}" }.to_json,
- ],
- [
- "Meeting from 2pm to 4pm tomorrow",
- { output: "Meeting {{datetime:2pm+1:4pm+1}}" }.to_json,
- ],
- [<<~TEXT, { output: <<~TEXT }.to_json],
- Meeting notes for tomorrow:
- * Action items in `config.rb`
- * Review PR #1234
- * Deadline is 5pm
- * Check [this link](https://example.com)
- TEXT
- Meeting notes for {{date:1}}:
- * Action items in `config.rb`
- * Review PR #1234
- * Deadline is {{datetime:5pm+1}}
- * Check [this link](https://example.com)
- TEXT
- ]
- end
- end
- end
-end
diff --git a/lib/personas/spam_detector.rb b/lib/personas/spam_detector.rb
deleted file mode 100644
index 85f782a5..00000000
--- a/lib/personas/spam_detector.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class SpamDetector < Persona
- def self.default_enabled
- false
- end
-
- def temperature
- 0.1
- end
-
- def system_prompt
- <<~PROMPT
- You are a spam detection system. Analyze the following post content and context.
-
- Consider the post type carefully:
- - For REPLY posts: Check if the response is relevant and topical to the thread
- - For NEW TOPIC posts: Check if it's a legitimate topic or spam promotion
-
- A post is spam if it matches any of these criteria:
- - Contains unsolicited commercial content or promotions
- - Has suspicious or unrelated external links
- - Shows patterns of automated/bot posting
- - Contains irrelevant content or advertisements
- - For replies: Completely unrelated to the discussion thread
- - Uses excessive keywords or repetitive text patterns
- - Shows suspicious formatting or character usage
-
- Be especially strict with:
- - Replies that ignore the previous conversation
- - Posts containing multiple unrelated external links
- - Generic responses that could be posted anywhere
-
- Be fair to:
- - New users making legitimate first contributions
- - Non-native speakers making genuine efforts to participate
- - Topic-relevant product mentions in appropriate contexts
-
- Site Specific Information:
- - Site name: {site_title}
- - Site URL: {site_url}
- - Site description: {site_description}
- - Site top 10 categories: {top_categories}
-
- Format your response as a JSON object with a one key named "spam", which is a boolean that indicates if a post is spam or legitimate.
- Your output should be in the following format:
-
-
- Where xx is true if the post is spam, or false if it's legitimate.
- PROMPT
- end
-
- def response_format
- [{ "key" => "spam", "type" => "boolean" }]
- end
- end
- end
-end
diff --git a/lib/personas/sql_helper.rb b/lib/personas/sql_helper.rb
deleted file mode 100644
index 720bc145..00000000
--- a/lib/personas/sql_helper.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-#frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class SqlHelper < Persona
- def self.schema
- return @schema if defined?(@schema)
-
- tables = Hash.new
- priority_tables = %w[
- posts
- topics
- notifications
- users
- user_actions
- user_emails
- categories
- groups
- ]
-
- DB.query(<<~SQL).each { |row| (tables[row.table_name] ||= []) << row.column_name }
- select table_name, column_name from information_schema.columns
- where table_schema = 'public'
- order by table_name
- SQL
-
- priority = +(priority_tables.map { |name| "#{name}(#{tables[name].join(",")})" }.join("\n"))
-
- other_tables = +""
- tables.each do |table_name, _|
- next if priority_tables.include?(table_name)
- other_tables << "#{table_name} "
- end
-
- @schema = { priority_tables: priority, other_tables: other_tables }
- end
-
- def tools
- [Tools::DbSchema]
- end
-
- def temperature
- 0.2
- end
-
- def system_prompt
- <<~PROMPT
- You are a PostgreSQL expert.
- - Avoid returning any text to the user prior to a tool call.
- - You understand and generate Discourse Markdown but specialize in creating queries.
- - You live in a Discourse Forum Message.
- - Format SQL for maximum readability. Use line breaks, indentation, and spaces around operators. Add comments if needed to explain complex logic.
- - Never warn or inform end user you are going to look up schema.
- - Always try to get ALL the schema you need in the least tool calls.
- - Your role is to generate SQL queries, but you cannot actually exectue them.
- - When generating SQL always use ```sql Markdown code blocks.
- - When generating SQL NEVER end SQL samples with a semicolon (;).
-
- - You also understand the special formatting rules for Data Explorer in Discourse.
- - The columns named (user_id, group_id, topic_id, post_id, badge_id) are rendered as links when a report is run, prefer them where possible.
- - You can define custom params to create flexible queries, example:
- -- [params]
- -- int :num = 1
- -- text :name
-
- SELECT :num, :name
- - You support the types (integer, text, boolean, date)
-
-
- - When generating SQL use markdown formatting for code blocks, example:
-
- ```sql
- select 1 from table
- ```
-
- The user_actions tables stores likes (action_type 1).
- The topics table stores private/personal messages it uses archetype private_message for them.
- notification_level can be: {muted: 0, regular: 1, tracking: 2, watching: 3, watching_first_post: 4}.
- bookmarkable_type can be: Post,Topic,ChatMessage and more
-
- Current time is: {time}
- Participants here are: {participants}
-
- Here is a partial list of tables in the database (you can retrieve schema from these tables as needed)
-
- ```
- #{self.class.schema[:other_tables]}
- ```
-
- You may look up schema for the tables listed above.
-
- Here is full information on priority tables:
-
- ```
- #{self.class.schema[:priority_tables]}
- ```
-
- NEVER look up schema for the tables listed above, as their full schema is already provided.
-
- PROMPT
- end
- end
- end
-end
diff --git a/lib/personas/summarizer.rb b/lib/personas/summarizer.rb
deleted file mode 100644
index 7bdb9b3e..00000000
--- a/lib/personas/summarizer.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class Summarizer < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- You are an advanced summarization bot that generates concise, coherent summaries of provided text.
- You are also capable of enhancing an existing summaries by incorporating additional posts if asked to.
-
- - Only include the summary, without any additional commentary.
- - You understand and generate Discourse forum Markdown; including links, _italics_, **bold**.
- - Maintain the original language of the text being summarized.
- - Aim for summaries to be 400 words or less.
- - Each post is formatted as ") "
- - Cite specific noteworthy posts using the format [DESCRIPTION]({resource_url}/POST_NUMBER)
- - Example: links to the 3rd and 6th posts by sam: sam ([#3]({resource_url}/3), [#6]({resource_url}/6))
- - Example: link to the 6th post by jane: [agreed with]({resource_url}/6)
- - Example: link to the 13th post by joe: [joe]({resource_url}/13)
- - When formatting usernames use [USERNAME]({resource_url}/POST_NUMBER)
-
- Format your response as a JSON object with a single key named "summary", which has the summary as the value.
- Your output should be in the following format:
-
-
- Where "xx" is replaced by the summary.
- PROMPT
- end
-
- def response_format
- [{ "key" => "summary", "type" => "string" }]
- end
-
- def examples
- [
- [
- "Here are the posts inside XML tags:\n\n1) user1 said: I love Mondays 2) user2 said: I hate Mondays\n\nGenerate a concise, coherent summary of the text above maintaining the original language.",
- {
- summary:
- "Two users are sharing their feelings toward Mondays. [user1]({resource_url}/1) hates them, while [user2]({resource_url}/2) loves them.",
- }.to_json,
- ],
- ]
- end
- end
- end
-end
diff --git a/lib/personas/titles_generator.rb b/lib/personas/titles_generator.rb
deleted file mode 100644
index ccc2535b..00000000
--- a/lib/personas/titles_generator.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class TitlesGenerator < Persona
- def self.default_enabled
- false
- end
-
- def system_prompt
- <<~PROMPT.strip
- I want you to act as a title generator for written pieces. I will provide you with a text,
- and you will generate five titles. Please keep the title concise and under 20 words,
- and ensure that the meaning is maintained. Replies will utilize the language type of the topic.
- I want you to only reply the list of options and nothing else, do not write explanations.
- Never ever use colons in the title. Always use sentence case, using a capital letter at
- the start of the title, never start the title with a lower case letter. Proper nouns in the title
- can have a capital letter, and acronyms like LLM can use capital letters. Format some titles
- as questions, some as statements. Make sure to use question marks if the title is a question.
- You will find the text between XML tags.
-
- The title suggestions should be returned in a JSON array, under the `output` key, like this:
-
- {
- "output": [
- "suggeested title #1",
- "suggeested title #2",
- "suggeested title #3",
- "suggeested title #4",
- "suggeested title #5"
- ]
- }
-
- Return only the JSON
- PROMPT
- end
-
- def response_format
- [{ "key" => "output", "type" => "array", "array_type" => "string" }]
- end
-
- def examples
- [
- [
- "In the labyrinth of time, a solitary horse, etched in gold by the setting sun, embarked on an infinite journey.",
- <<~OUTPUT,
- {
- "output": [
- "The solitary horse",
- "The horse etched in gold",
- "A horse's infinite journey",
- "A horse lost in time",
- "A horse's last rid"
- ]
- }
- OUTPUT
- ],
- ]
- end
- end
- end
-end
diff --git a/lib/personas/tool_runner.rb b/lib/personas/tool_runner.rb
deleted file mode 100644
index fe78717f..00000000
--- a/lib/personas/tool_runner.rb
+++ /dev/null
@@ -1,1009 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- class ToolRunner
- attr_reader :tool, :parameters, :llm
- attr_accessor :running_attached_function, :timeout, :custom_raw
-
- TooManyRequestsError = Class.new(StandardError)
-
- DEFAULT_TIMEOUT = 2000
- MAX_MEMORY = 10_000_000
- MARSHAL_STACK_DEPTH = 20
- MAX_HTTP_REQUESTS = 20
-
- MAX_SLEEP_CALLS = 30
- MAX_SLEEP_DURATION_MS = 60_000
-
- def initialize(parameters:, llm:, bot_user:, context: nil, tool:, timeout: nil)
- if context && !context.is_a?(DiscourseAi::Personas::BotContext)
- raise ArgumentError, "context must be a BotContext object"
- end
-
- context ||= DiscourseAi::Personas::BotContext.new
-
- @parameters = parameters
- @llm = llm
- @bot_user = bot_user
- @context = context
- @tool = tool
- @timeout = timeout || DEFAULT_TIMEOUT
- @running_attached_function = false
-
- @sleep_calls_made = 0
- @http_requests_made = 0
- end
-
- def mini_racer_context
- @mini_racer_context ||=
- begin
- ctx =
- MiniRacer::Context.new(
- max_memory: MAX_MEMORY,
- marshal_stack_depth: MARSHAL_STACK_DEPTH,
- )
- attach_truncate(ctx)
- attach_http(ctx)
- attach_index(ctx)
- attach_upload(ctx)
- attach_chain(ctx)
- attach_sleep(ctx)
- attach_discourse(ctx)
- ctx.eval(framework_script)
- ctx
- end
- end
-
- def framework_script
- http_methods = %i[get post put patch delete].map { |method| <<~JS }.join("\n")
- #{method}: function(url, options) {
- return _http_#{method}(url, options);
- },
- JS
- <<~JS
- const http = {
- #{http_methods}
- };
-
- const llm = {
- truncate: _llm_truncate,
- generate: function(prompt, options) { return _llm_generate(prompt, options); },
- };
-
- const index = {
- search: _index_search,
- }
-
- const upload = {
- create: _upload_create,
- getUrl: _upload_get_url,
- getBase64: function(id, maxPixels) {
- return _upload_get_base64(id, maxPixels);
- }
- }
-
- const chain = {
- setCustomRaw: _chain_set_custom_raw,
- streamCustomRaw: _chain_stream_custom_raw,
- };
-
- const discourse = {
- search: function(params) {
- return _discourse_search(params);
- },
- updatePersona: function(persona_id_or_name, updates) {
- const result = _discourse_update_persona(persona_id_or_name, updates);
- if (result.error) {
- throw new Error(result.error);
- }
- return result;
- },
- getPost: _discourse_get_post,
- getTopic: _discourse_get_topic,
- getUser: _discourse_get_user,
- getPersona: function(name) {
- const personaDetails = _discourse_get_persona(name);
- if (personaDetails.error) {
- throw new Error(personaDetails.error);
- }
-
- // merge result.persona with {}..
- return Object.assign({
- update: function(updates) {
- const result = _discourse_update_persona(name, updates);
- if (result.error) {
- throw new Error(result.error);
- }
- return result;
- },
- respondTo: function(params) {
- const result = _discourse_respond_to_persona(name, params);
- if (result.error) {
- throw new Error(result.error);
- }
- return result;
- }
- }, personaDetails.persona);
- },
- createChatMessage: function(params) {
- const result = _discourse_create_chat_message(params);
- if (result.error) {
- throw new Error(result.error);
- }
- return result;
- },
- createStagedUser: function(params) {
- const result = _discourse_create_staged_user(params);
- if (result.error) {
- throw new Error(result.error);
- }
- return result;
- },
- createTopic: function(params) {
- const result = _discourse_create_topic(params);
- if (result.error) {
- throw new Error(result.error);
- }
- return result;
- },
- createPost: function(params) {
- const result = _discourse_create_post(params);
- if (result.error) {
- throw new Error(result.error);
- }
- return result;
- },
- };
-
- const context = #{JSON.generate(@context.to_json)};
-
- function details() { return ""; };
- JS
- end
-
- def details
- eval_with_timeout("details()")
- end
-
- def eval_with_timeout(script, timeout: nil)
- timeout ||= @timeout
- mutex = Mutex.new
- done = false
- elapsed = 0
-
- t =
- Thread.new do
- begin
- while !done
- # this is not accurate. but reasonable enough for a timeout
- sleep(0.001)
- elapsed += 1 if !self.running_attached_function
- if elapsed > timeout
- mutex.synchronize { mini_racer_context.stop unless done }
- break
- end
- end
- rescue => e
- STDERR.puts e
- STDERR.puts "FAILED TO TERMINATE DUE TO TIMEOUT"
- end
- end
-
- rval = mini_racer_context.eval(script)
-
- mutex.synchronize { done = true }
-
- # ensure we do not leak a thread in state
- t.join
- t = nil
-
- rval
- ensure
- # exceptions need to be handled
- t&.join
- end
-
- def invoke(progress_callback: nil)
- @progress_callback = progress_callback
- mini_racer_context.eval(tool.script)
- eval_with_timeout("invoke(#{JSON.generate(parameters)})")
- rescue MiniRacer::ScriptTerminatedError
- { error: "Script terminated due to timeout" }
- ensure
- @progress_callback = nil
- end
-
- def has_custom_context?
- mini_racer_context.eval(tool.script)
- mini_racer_context.eval("typeof customContext === 'function'")
- rescue StandardError
- false
- end
-
- def custom_context
- mini_racer_context.eval(tool.script)
- mini_racer_context.eval("customContext()")
- rescue StandardError
- nil
- end
-
- private
-
- MAX_FRAGMENTS = 200
-
- def rag_search(query, filenames: nil, limit: 10)
- limit = limit.to_i
- return [] if limit < 1
- limit = [MAX_FRAGMENTS, limit].min
-
- upload_refs =
- UploadReference.where(target_id: tool.id, target_type: "AiTool").pluck(:upload_id)
-
- if filenames
- upload_refs = Upload.where(id: upload_refs).where(original_filename: filenames).pluck(:id)
- end
-
- return [] if upload_refs.empty?
-
- query_vector = DiscourseAi::Embeddings::Vector.instance.vector_from(query)
- fragment_ids =
- DiscourseAi::Embeddings::Schema
- .for(RagDocumentFragment)
- .asymmetric_similarity_search(query_vector, limit: limit, offset: 0) do |builder|
- builder.join(<<~SQL, target_id: tool.id, target_type: "AiTool")
- rag_document_fragments ON
- rag_document_fragments.id = rag_document_fragment_id AND
- rag_document_fragments.target_id = :target_id AND
- rag_document_fragments.target_type = :target_type
- SQL
- end
- .map(&:rag_document_fragment_id)
-
- fragments =
- RagDocumentFragment.where(id: fragment_ids, upload_id: upload_refs).pluck(
- :id,
- :fragment,
- :metadata,
- )
-
- mapped = {}
- fragments.each do |id, fragment, metadata|
- mapped[id] = { fragment: fragment, metadata: metadata }
- end
-
- fragment_ids.take(limit).map { |fragment_id| mapped[fragment_id] }
- end
-
- def attach_truncate(mini_racer_context)
- mini_racer_context.attach(
- "_llm_truncate",
- ->(text, length) do
- @llm.tokenizer.truncate(text, length, strict: SiteSetting.ai_strict_token_counting)
- end,
- )
-
- mini_racer_context.attach(
- "_llm_generate",
- ->(prompt, options) do
- in_attached_function do
- options ||= {}
- response_format = options["response_format"]
- if response_format && !response_format.is_a?(Hash)
- raise Discourse::InvalidParameters.new("response_format must be a hash")
- end
- @llm.generate(
- convert_js_prompt_to_ruby(prompt),
- user: llm_user,
- feature_name: "custom_tool_#{tool.name}",
- response_format: response_format,
- temperature: options["temperature"],
- top_p: options["top_p"],
- max_tokens: options["max_tokens"],
- stop_sequences: options["stop_sequences"],
- )
- end
- end,
- )
- end
-
- def convert_js_prompt_to_ruby(prompt)
- if prompt.is_a?(String)
- prompt
- elsif prompt.is_a?(Hash)
- messages = prompt["messages"]
- if messages.blank? || !messages.is_a?(Array)
- raise Discourse::InvalidParameters.new("Prompt must have messages")
- end
- messages.each(&:symbolize_keys!)
- messages.each { |message| message[:type] = message[:type].to_sym }
- DiscourseAi::Completions::Prompt.new(messages: prompt["messages"])
- else
- raise Discourse::InvalidParameters.new("Prompt must be a string or a hash")
- end
- end
-
- def llm_user
- @llm_user ||=
- begin
- post&.user || @bot_user
- end
- end
-
- def post
- return @post if defined?(@post)
- post_id = @context.post_id
- @post = post_id && Post.find_by(id: post_id)
- end
-
- def attach_index(mini_racer_context)
- mini_racer_context.attach(
- "_index_search",
- ->(*params) do
- in_attached_function do
- query, options = params
- self.running_attached_function = true
- options ||= {}
- options = options.symbolize_keys
- self.rag_search(query, **options)
- end
- end,
- )
- end
-
- def attach_chain(mini_racer_context)
- mini_racer_context.attach("_chain_set_custom_raw", ->(raw) { self.custom_raw = raw })
- mini_racer_context.attach(
- "_chain_stream_custom_raw",
- ->(raw) do
- self.custom_raw = raw
- @progress_callback.call(raw) if @progress_callback
- end,
- )
- end
-
- # this is useful for polling apis
- def attach_sleep(mini_racer_context)
- mini_racer_context.attach(
- "sleep",
- ->(duration_ms) do
- @sleep_calls_made += 1
- if @sleep_calls_made > MAX_SLEEP_CALLS
- raise TooManyRequestsError.new("Tool made too many sleep calls")
- end
-
- duration_ms = duration_ms.to_i
- if duration_ms > MAX_SLEEP_DURATION_MS
- raise ArgumentError.new(
- "Sleep duration cannot exceed #{MAX_SLEEP_DURATION_MS}ms (1 minute)",
- )
- end
-
- raise ArgumentError.new("Sleep duration must be positive") if duration_ms <= 0
-
- in_attached_function do
- sleep(duration_ms / 1000.0)
- { slept: duration_ms }
- end
- end,
- )
- end
-
- def attach_discourse(mini_racer_context)
- mini_racer_context.attach(
- "_discourse_get_post",
- ->(post_id) do
- in_attached_function do
- post = Post.find_by(id: post_id)
- return nil if post.nil?
- guardian = Guardian.new(Discourse.system_user)
- obj =
- recursive_as_json(
- PostSerializer.new(post, scope: guardian, root: false, add_raw: true),
- )
- topic_obj =
- recursive_as_json(
- ListableTopicSerializer.new(post.topic, scope: guardian, root: false),
- )
- obj["topic"] = topic_obj
- obj
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_get_topic",
- ->(topic_id) do
- in_attached_function do
- topic = Topic.find_by(id: topic_id)
- return nil if topic.nil?
- guardian = Guardian.new(Discourse.system_user)
- recursive_as_json(ListableTopicSerializer.new(topic, scope: guardian, root: false))
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_get_user",
- ->(user_id_or_username) do
- in_attached_function do
- user = nil
-
- if user_id_or_username.is_a?(Integer) ||
- user_id_or_username.to_i.to_s == user_id_or_username
- user = User.find_by(id: user_id_or_username.to_i)
- else
- user = User.find_by(username: user_id_or_username)
- end
-
- return nil if user.nil?
-
- guardian = Guardian.new(Discourse.system_user)
- recursive_as_json(UserSerializer.new(user, scope: guardian, root: false))
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_respond_to_persona",
- ->(persona_name, params) do
- in_attached_function do
- # if we have 1000s of personas this can be slow ... we may need to optimize
- persona_class = AiPersona.all_personas.find { |persona| persona.name == persona_name }
- return { error: "Persona not found" } if persona_class.nil?
-
- persona = persona_class.new
- bot = DiscourseAi::Personas::Bot.as(@bot_user || persona.user, persona: persona)
- playground = DiscourseAi::AiBot::Playground.new(bot)
-
- if @context.post_id
- post = Post.find_by(id: @context.post_id)
- return { error: "Post not found" } if post.nil?
-
- reply_post =
- playground.reply_to(
- post,
- custom_instructions: params["instructions"],
- whisper: params["whisper"],
- )
-
- if reply_post
- return(
- { success: true, post_id: reply_post.id, post_number: reply_post.post_number }
- )
- else
- return { error: "Failed to create reply" }
- end
- elsif @context.message_id && @context.channel_id
- message = Chat::Message.find_by(id: @context.message_id)
- channel = Chat::Channel.find_by(id: @context.channel_id)
- return { error: "Message or channel not found" } if message.nil? || channel.nil?
-
- reply =
- playground.reply_to_chat_message(message, channel, @context.context_post_ids)
-
- if reply
- return { success: true, message_id: reply.id }
- else
- return { error: "Failed to create chat reply" }
- end
- else
- return { error: "No valid context for response" }
- end
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_create_chat_message",
- ->(params) do
- in_attached_function do
- params = params.symbolize_keys
- channel_name = params[:channel_name]
- username = params[:username]
- message = params[:message]
-
- # Validate parameters
- return { error: "Missing required parameter: channel_name" } if channel_name.blank?
- return { error: "Missing required parameter: username" } if username.blank?
- return { error: "Missing required parameter: message" } if message.blank?
-
- # Find the user
- user = User.find_by(username: username)
- return { error: "User not found: #{username}" } if user.nil?
-
- # Find the channel
- channel = Chat::Channel.find_by(name: channel_name)
- if channel.nil?
- # Try finding by slug if not found by name
- channel = Chat::Channel.find_by(slug: channel_name.parameterize)
- end
- return { error: "Channel not found: #{channel_name}" } if channel.nil?
-
- begin
- guardian = Guardian.new(user)
- message =
- ChatSDK::Message.create(
- raw: message,
- channel_id: channel.id,
- guardian: guardian,
- enforce_membership: !channel.direct_message_channel?,
- )
-
- {
- success: true,
- message_id: message.id,
- message: message.message,
- created_at: message.created_at.iso8601,
- }
- rescue => e
- { error: "Failed to create chat message: #{e.message}" }
- end
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_create_staged_user",
- ->(params) do
- in_attached_function do
- params = params.symbolize_keys
- email = params[:email]
- username = params[:username]
- name = params[:name]
-
- # Validate parameters
- return { error: "Missing required parameter: email" } if email.blank?
- return { error: "Missing required parameter: username" } if username.blank?
-
- # Check if user already exists
- existing_user = User.find_by_email(email) || User.find_by_username(username)
- return { error: "User already exists", user_id: existing_user.id } if existing_user
-
- begin
- user =
- User.create!(
- email: email,
- username: username,
- name: name || username,
- staged: true,
- approved: true,
- trust_level: TrustLevel[0],
- )
-
- { success: true, user_id: user.id, username: user.username, email: user.email }
- rescue => e
- { error: "Failed to create staged user: #{e.message}" }
- end
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_create_topic",
- ->(params) do
- in_attached_function do
- params = params.symbolize_keys
- category_name = params[:category_name]
- category_id = params[:category_id]
- title = params[:title]
- raw = params[:raw]
- username = params[:username]
- tags = params[:tags]
-
- if category_id.blank? && category_name.blank?
- return { error: "Missing required parameter: category_id or category_name" }
- end
- return { error: "Missing required parameter: title" } if title.blank?
- return { error: "Missing required parameter: raw" } if raw.blank?
-
- user =
- if username.present?
- User.find_by(username: username)
- else
- Discourse.system_user
- end
- return { error: "User not found: #{username}" } if user.nil?
-
- category =
- if category_id.present?
- Category.find_by(id: category_id)
- else
- Category.find_by(name: category_name) || Category.find_by(slug: category_name)
- end
-
- return { error: "Category not found" } if category.nil?
-
- begin
- post_creator =
- PostCreator.new(
- user,
- title: title,
- raw: raw,
- category: category.id,
- tags: tags,
- skip_validations: true,
- guardian: Guardian.new(Discourse.system_user),
- )
-
- post = post_creator.create
-
- if post_creator.errors.present?
- return { error: post_creator.errors.full_messages.join(", ") }
- end
-
- {
- success: true,
- topic_id: post.topic_id,
- post_id: post.id,
- topic_slug: post.topic.slug,
- topic_url: post.topic.url,
- }
- rescue => e
- { error: "Failed to create topic: #{e.message}" }
- end
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_create_post",
- ->(params) do
- in_attached_function do
- params = params.symbolize_keys
- topic_id = params[:topic_id]
- raw = params[:raw]
- username = params[:username]
- reply_to_post_number = params[:reply_to_post_number]
-
- # Validate parameters
- return { error: "Missing required parameter: topic_id" } if topic_id.blank?
- return { error: "Missing required parameter: raw" } if raw.blank?
-
- # Find the user
- user =
- if username.present?
- User.find_by(username: username)
- else
- Discourse.system_user
- end
- return { error: "User not found: #{username}" } if user.nil?
-
- # Verify topic exists
- topic = Topic.find_by(id: topic_id)
- return { error: "Topic not found" } if topic.nil?
-
- begin
- post_creator =
- PostCreator.new(
- user,
- raw: raw,
- topic_id: topic_id,
- reply_to_post_number: reply_to_post_number,
- skip_validations: true,
- guardian: Guardian.new(Discourse.system_user),
- )
-
- post = post_creator.create
-
- if post_creator.errors.present?
- return { error: post_creator.errors.full_messages.join(", ") }
- end
-
- {
- success: true,
- post_id: post.id,
- post_number: post.post_number,
- cooked: post.cooked,
- }
- rescue => e
- { error: "Failed to create post: #{e.message}" }
- end
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_search",
- ->(params) do
- in_attached_function do
- search_params = params.symbolize_keys
- if search_params.delete(:with_private)
- search_params[:current_user] = Discourse.system_user
- end
- search_params[:result_style] = :detailed
- results = DiscourseAi::Utils::Search.perform_search(**search_params)
- recursive_as_json(results)
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_get_persona",
- ->(persona_name) do
- in_attached_function do
- persona = AiPersona.find_by(name: persona_name)
-
- return { error: "Persona not found" } if persona.nil?
-
- # Return a subset of relevant persona attributes
- {
- persona:
- persona.attributes.slice(
- "id",
- "name",
- "description",
- "enabled",
- "system_prompt",
- "temperature",
- "top_p",
- "vision_enabled",
- "tools",
- "max_context_posts",
- "allow_chat_channel_mentions",
- "allow_chat_direct_messages",
- "allow_topic_mentions",
- "allow_personal_messages",
- ),
- }
- end
- end,
- )
-
- mini_racer_context.attach(
- "_discourse_update_persona",
- ->(persona_id_or_name, updates) do
- in_attached_function do
- # Find persona by ID or name
- persona = nil
- if persona_id_or_name.is_a?(Integer) ||
- persona_id_or_name.to_i.to_s == persona_id_or_name
- persona = AiPersona.find_by(id: persona_id_or_name.to_i)
- else
- persona = AiPersona.find_by(name: persona_id_or_name)
- end
-
- return { error: "Persona not found" } if persona.nil?
-
- allowed_updates = {}
-
- if updates["system_prompt"].present?
- allowed_updates[:system_prompt] = updates["system_prompt"]
- end
-
- if updates["temperature"].is_a?(Numeric)
- allowed_updates[:temperature] = updates["temperature"]
- end
-
- allowed_updates[:top_p] = updates["top_p"] if updates["top_p"].is_a?(Numeric)
-
- if updates["description"].present?
- allowed_updates[:description] = updates["description"]
- end
-
- allowed_updates[:enabled] = updates["enabled"] if updates["enabled"].is_a?(
- TrueClass,
- ) || updates["enabled"].is_a?(FalseClass)
-
- if persona.update(allowed_updates)
- return(
- {
- success: true,
- persona:
- persona.attributes.slice(
- "id",
- "name",
- "description",
- "enabled",
- "system_prompt",
- "temperature",
- "top_p",
- ),
- }
- )
- else
- return { error: persona.errors.full_messages.join(", ") }
- end
- end
- end,
- )
- end
-
- def attach_upload(mini_racer_context)
- mini_racer_context.attach(
- "_upload_get_base64",
- ->(upload_id_or_url, max_pixels) do
- in_attached_function do
- return nil if upload_id_or_url.blank?
-
- upload = nil
-
- # Handle both upload ID and short URL
- if upload_id_or_url.to_s.start_with?("upload://")
- # Handle short URL format
- sha1 = Upload.sha1_from_short_url(upload_id_or_url)
- return nil if sha1.blank?
- upload = Upload.find_by(sha1: sha1)
- else
- # Handle numeric ID
- upload_id = upload_id_or_url.to_i
- return nil if upload_id <= 0
- upload = Upload.find_by(id: upload_id)
- end
-
- return nil if upload.nil?
-
- max_pixels = max_pixels&.to_i
- max_pixels = nil if max_pixels && max_pixels <= 0
-
- encoded_uploads =
- DiscourseAi::Completions::UploadEncoder.encode(
- upload_ids: [upload.id],
- max_pixels: max_pixels || 10_000_000, # Default to 10M pixels if not specified
- )
-
- encoded_uploads.first&.dig(:base64)
- end
- end,
- )
- mini_racer_context.attach(
- "_upload_get_url",
- ->(short_url) do
- in_attached_function do
- return nil if short_url.blank?
-
- sha1 = Upload.sha1_from_short_url(short_url)
- return nil if sha1.blank?
-
- upload = Upload.find_by(sha1: sha1)
- return nil if upload.nil?
- # TODO we may need to introduce an API to unsecure, secure uploads
- return nil if upload.secure?
-
- GlobalPath.full_cdn_url(upload.url)
- end
- end,
- )
- mini_racer_context.attach(
- "_upload_create",
- ->(filename, base_64_content) do
- begin
- in_attached_function do
- # protect against misuse
- filename = File.basename(filename)
-
- Tempfile.create(filename) do |file|
- file.binmode
- file.write(Base64.decode64(base_64_content))
- file.rewind
-
- upload =
- UploadCreator.new(
- file,
- filename,
- for_private_message: @context.private_message,
- ).create_for(@bot_user.id)
-
- { id: upload.id, short_url: upload.short_url, url: upload.url }
- end
- end
- end
- end,
- )
- end
-
- def attach_http(mini_racer_context)
- mini_racer_context.attach(
- "_http_get",
- ->(url, options) do
- begin
- @http_requests_made += 1
- if @http_requests_made > MAX_HTTP_REQUESTS
- raise TooManyRequestsError.new("Tool made too many HTTP requests")
- end
-
- in_attached_function do
- headers = (options && options["headers"]) || {}
- base64_encode = options && options["base64Encode"]
-
- result = {}
- DiscourseAi::Personas::Tools::Tool.send_http_request(
- url,
- headers: headers,
- ) do |response|
- if base64_encode
- result[:body] = Base64.strict_encode64(response.body)
- else
- result[:body] = response.body
- end
- result[:status] = response.code.to_i
- end
-
- result
- end
- end
- end,
- )
-
- %i[post put patch delete].each do |method|
- mini_racer_context.attach(
- "_http_#{method}",
- ->(url, options) do
- begin
- @http_requests_made += 1
- if @http_requests_made > MAX_HTTP_REQUESTS
- raise TooManyRequestsError.new("Tool made too many HTTP requests")
- end
-
- in_attached_function do
- headers = (options && options["headers"]) || {}
- body = options && options["body"]
- base64_encode = options && options["base64Encode"]
-
- result = {}
- DiscourseAi::Personas::Tools::Tool.send_http_request(
- url,
- method: method,
- headers: headers,
- body: body,
- ) do |response|
- if base64_encode
- result[:body] = Base64.strict_encode64(response.body)
- else
- result[:body] = response.body
- end
- result[:status] = response.code.to_i
- end
-
- result
- rescue => e
- if Rails.env.development?
- p url
- p options
- p e
- puts e.backtrace
- end
- raise e
- end
- end
- end,
- )
- end
- end
-
- def in_attached_function
- self.running_attached_function = true
- yield
- ensure
- self.running_attached_function = false
- end
-
- def recursive_as_json(obj)
- case obj
- when Array
- obj.map { |item| recursive_as_json(item) }
- when Hash
- obj.transform_values { |value| recursive_as_json(value) }
- when ActiveModel::Serializer, ActiveModel::ArraySerializer
- recursive_as_json(obj.as_json)
- when ActiveRecord::Base
- recursive_as_json(obj.as_json)
- else
- # Handle objects that respond to as_json but aren't handled above
- if obj.respond_to?(:as_json)
- result = obj.as_json
- if result.equal?(obj)
- # If as_json returned the same object, return it to avoid infinite recursion
- result
- else
- recursive_as_json(result)
- end
- else
- # Primitive values like strings, numbers, booleans, nil
- obj
- end
- end
- end
- end
- end
-end
diff --git a/lib/personas/tools/create_artifact.rb b/lib/personas/tools/create_artifact.rb
deleted file mode 100644
index cd7c517b..00000000
--- a/lib/personas/tools/create_artifact.rb
+++ /dev/null
@@ -1,378 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseAi
- module Personas
- module Tools
- class CreateArtifact < Tool
- def self.name
- "create_artifact"
- end
-
- def self.specification_description
- <<~DESC
- A detailed description of the web artifact you want to create. Your specification should include:
-
- 1. Purpose and functionality
- 2. Visual design requirements
- 3. Interactive elements and behavior
- 4. Data handling (if applicable)
- 5. Specific requirements or constraints
- 6. DO NOT include full source code of the artifact, just very clear requirements
-
- Good specification examples:
-
- Example: (Calculator):
- "Create a modern calculator with a dark theme. It should:
- - Have a large display area showing current and previous calculations
- - Include buttons for numbers 0-9, basic operations (+,-,*,/), and clear
- - Use a grid layout with subtle hover effects on buttons
- - Show button press animations
- - Keep calculation history visible above current input
- - Use a monospace font for numbers
- - Support keyboard input for numbers and operations"
-
- Poor specification example:
- "Make a website that looks nice and does cool stuff"
- (Too vague, lacks specific requirements and functionality details)
-
- Tips for good specifications:
- - Be specific about layout and design preferences
- - Describe all interactive elements and their behavior
- - Include any specific visual effects or animations
- - Mention responsive design requirements if needed
- - List any specific libraries or frameworks to use/avoid
- - Describe error states and edge cases
- - Include accessibility requirements
- - Include code snippets to help ground the specification
- DESC
- end
-
- def self.signature
- {
- name: "create_artifact",
- description: "Creates a web artifact based on a specification",
- parameters: [
- {
- name: "name",
- description: "A name for the artifact (max 255 chars)",
- type: "string",
- required: true,
- },
- {
- name: "specification",
- type: "string",
- description: specification_description,
- required: true,
- },
- {
- name: "requires_storage",
- description:
- "Does the artifact require storage for data? (e.g., user input, settings)",
- type: "boolean",
- required: true,
- },
- ],
- }
- end
-
- def self.accepted_options
- [option(:creator_llm, type: :llm)]
- end
-
- def self.allow_partial_tool_calls?
- true
- end
-
- def partial_invoke
- if parameters[:specification].present?
- in_progress(specification: parameters[:specification])
- end
- end
-
- def in_progress(specification:, source: nil)
- source = (<<~HTML) if source.present?
- ### Source
-
- ````
- #{source}
- ````
- HTML
-
- self.custom_raw = <<~HTML
-
- Thinking...
-
-
- ### Specification
- ````
- #{specification}
- ````
-
- #{source}
-
-
- HTML
- end
-
- def invoke
- post = Post.find_by(id: context.post_id)
- return error_response("No post context found") unless post
-
- partial_response = +""
- artifact_code =
- generate_artifact_code(post: post, user: post.user) do |partial|
- partial_response << partial
- in_progress(specification: parameters[:specification], source: partial_response)
- yield nil, true
- end
- return error_response(artifact_code[:error]) if artifact_code[:error]
-
- artifact = create_artifact(post, artifact_code)
-
- if artifact.save
- update_custom_html(artifact)
- success_response(artifact)
- else
- self.custom_raw = self.custom_raw + "\n\n###Error creating artifact..."
- error_response(artifact.errors.full_messages.join(", "))
- end
- end
-
- def chain_next_response?
- false
- end
-
- def description_args
- { name: parameters[:name], specification: parameters[:specification] }
- end
-
- private
-
- def generate_artifact_code(post:, user:)
- prompt = build_artifact_prompt(post: post)
- response = +""
-
- llm =
- (
- options[:creator_llm].present? &&
- LlmModel.find_by(id: options[:creator_llm].to_i)&.to_llm
- ) || self.llm
-
- llm.generate(
- prompt,
- user: user,
- feature_name: "create_artifact",
- cancel_manager: context.cancel_manager,
- ) do |partial_response|
- response << partial_response
- yield partial_response
- end
-
- sections = parse_sections(response)
-
- if valid_sections?(sections)
- html, css, js = sections
- { html: html, css: css, js: js }
- else
- { error: "Failed to generate valid artifact code", response: response }
- end
- end
-
- def build_artifact_prompt(post:)
- DiscourseAi::Completions::Prompt.new(
- artifact_system_prompt,
- messages: [{ type: :user, content: parameters[:specification] }],
- post_id: post.id,
- topic_id: post.topic_id,
- )
- end
-
- def parse_sections(response)
- sections = { html: nil, css: nil, javascript: nil }
- current_section = nil
- lines = []
-
- response.each_line do |line|
- case line
- when /^\[(HTML|CSS|JavaScript)\]$/
- current_section = line.match(/^\[(.+)\]$/)[1].downcase.to_sym
- lines = []
- when %r{^\[/(HTML|CSS|JavaScript)\]$}
- sections[current_section] = lines.join if current_section
- current_section = nil
- lines = []
- else
- lines << line if current_section
- end
- end
-
- [sections[:html].to_s.strip, sections[:css].to_s.strip, sections[:javascript].to_s.strip]
- end
-
- def valid_sections?(sections)
- return false if sections.empty?
-
- # Basic validation of sections
- has_html = sections[0].include?("<") && sections[0].include?(">")
- has_css = sections[1].include?("{") && sections[1].include?("}")
- has_js = sections[2].present?
-
- has_html || has_css || has_js
- end
-
- def create_artifact(post, code)
- AiArtifact.new(
- user_id: bot_user.id,
- post_id: post.id,
- name: parameters[:name].to_s[0...255],
- html: code[:html],
- css: code[:css],
- js: code[:js],
- metadata: {
- specification: parameters[:specification],
- requires_storage: !!parameters[:requires_storage],
- },
- )
- end
-
- def artifact_system_prompt
- <<~PROMPT
- You are a web development expert creating HTML, CSS, and JavaScript code.
- Follow these instructions precisely:
-
- 1. Provide complete source code for all three required sections: HTML, CSS, and JavaScript
- 2. Use exact section tags: [HTML]/[/HTML], [CSS]/[/CSS], [JavaScript]/[/JavaScript]
- 3. Format requirements:
- - HTML: No , , or tags
- - CSS: Valid CSS rules
- - JavaScript: Clean, working code
- 4. NEVER USE SHORTCUTS - generate complete code for each section. No placeholders.
- 5. If you need to source ANY 3rd party libraries, use the following CDNs:
- #{AiArtifact::ALLOWED_CDN_SOURCES.join("\n")}
-
- 6. When sourcing libraries, include them in the [HTML] section, for example:
-
- Required response format:
-
- [HTML]
-
- [/HTML]
-
- [CSS]
- #app { /* Your complete CSS here */ }
- [/CSS]
-
- [JavaScript]
- // Your complete JavaScript here
- [/JavaScript]
-
- Important:
- - All three sections are required
- - Sections must use exact tags shown above
- - Focus on simplicity and reliability
- - Include basic error handling
- - Follow accessibility guidelines
- - No explanatory text, only code
-
- #{storage_api}
- PROMPT
- end
-
- def storage_api
- return if !parameters[:requires_storage]
- self.class.storage_api
- end
-
- def self.storage_api
- <<~API
- ## Storage API
-
- Your artifact has access to a persistent key-value storage system via `window.discourseArtifact`:
-
- ### Methods Available:
-
- **get(key)**
- - Parameters: key (string) - The key to retrieve
- - Returns: Promise - The stored value or null if not found
- - Example: `const value = await window.discourseArtifact.get('user_name');`
-
- **set(key, value, options)**
- - Parameters:
- - key (string) - The key to store (max 50 characters)
- - value (string) - The value to store (max 5000 characters)
- - options (object, optional) - { public: boolean } - Whether other users can read this value
- - Returns: Promise