diff --git a/bot/admin/server/src/main/kotlin/model/genai/BotRAGConfigurationDTO.kt b/bot/admin/server/src/main/kotlin/model/genai/BotRAGConfigurationDTO.kt index 1214b137ad..adef95304d 100644 --- a/bot/admin/server/src/main/kotlin/model/genai/BotRAGConfigurationDTO.kt +++ b/bot/admin/server/src/main/kotlin/model/genai/BotRAGConfigurationDTO.kt @@ -42,9 +42,6 @@ data class BotRAGConfigurationDTO( val emSetting: EMSettingDTO, val indexSessionId: String? = null, val indexName: String? = null, - val noAnswerSentence: String, - val noAnswerStoryId: String? = null, - val documentsRequired: Boolean = true, val debugEnabled: Boolean, val maxDocumentsRetrieved: Int, val maxMessagesFromHistory: Int, @@ -63,9 +60,6 @@ data class BotRAGConfigurationDTO( emSetting = configuration.emSetting.toDTO(), indexSessionId = configuration.indexSessionId, indexName = configuration.generateIndexName(), - noAnswerSentence = configuration.noAnswerSentence, - noAnswerStoryId = configuration.noAnswerStoryId, - documentsRequired = configuration.documentsRequired, debugEnabled = configuration.debugEnabled, maxDocumentsRetrieved = configuration.maxDocumentsRetrieved, maxMessagesFromHistory = configuration.maxMessagesFromHistory, @@ -101,9 +95,6 @@ data class BotRAGConfigurationDTO( dto = emSetting, ), indexSessionId = indexSessionId, - noAnswerSentence = noAnswerSentence, - noAnswerStoryId = noAnswerStoryId, - documentsRequired = documentsRequired, debugEnabled = debugEnabled, maxDocumentsRetrieved = maxDocumentsRetrieved, maxMessagesFromHistory = maxMessagesFromHistory, diff --git a/bot/admin/server/src/test/kotlin/service/RAGServiceTest.kt b/bot/admin/server/src/test/kotlin/service/RAGServiceTest.kt index f2a2e36cde..d34d8ac5c0 100644 --- a/bot/admin/server/src/test/kotlin/service/RAGServiceTest.kt +++ b/bot/admin/server/src/test/kotlin/service/RAGServiceTest.kt @@ -97,8 +97,6 @@ class RAGServiceTest : AbstractTest() { model = "model", apiBase = "url", ), - noAnswerSentence = "No answer sentence", - documentsRequired = true, debugEnabled = false, maxDocumentsRetrieved = 2, maxMessagesFromHistory = 2, @@ -211,7 +209,6 @@ class RAGServiceTest : AbstractTest() { Assertions.assertEquals(PROVIDER, captured.questionAnsweringLlmSetting!!.provider.name) Assertions.assertEquals(TEMPERATURE, captured.questionAnsweringLlmSetting!!.temperature) Assertions.assertEquals(PROMPT, captured.questionAnsweringPrompt!!.template) - Assertions.assertEquals(null, captured.noAnswerStoryId) } TestCase("Save valid RAG Configuration that does not exist yet").given( diff --git a/bot/admin/server/src/test/kotlin/service/RAGValidationServiceTest.kt b/bot/admin/server/src/test/kotlin/service/RAGValidationServiceTest.kt index 0bdca1b629..bfa0dd9631 100644 --- a/bot/admin/server/src/test/kotlin/service/RAGValidationServiceTest.kt +++ b/bot/admin/server/src/test/kotlin/service/RAGValidationServiceTest.kt @@ -90,8 +90,6 @@ class RAGValidationServiceTest { questionAnsweringLlmSetting = openAILLMSetting, questionAnsweringPrompt = PromptTemplate(template = "How to bike in the rain"), emSetting = azureOpenAIEMSetting, - noAnswerSentence = " No answer sentence", - documentsRequired = true, debugEnabled = false, maxDocumentsRetrieved = 2, maxMessagesFromHistory = 2, diff --git a/bot/admin/web/src/app/analytics/dialogs/dialogs-list/dialogs-list-filters/dialogs-list-filters.component.ts b/bot/admin/web/src/app/analytics/dialogs/dialogs-list/dialogs-list-filters/dialogs-list-filters.component.ts index be812aa671..4272512856 100644 --- a/bot/admin/web/src/app/analytics/dialogs/dialogs-list/dialogs-list-filters/dialogs-list-filters.component.ts +++ b/bot/admin/web/src/app/analytics/dialogs/dialogs-list/dialogs-list-filters/dialogs-list-filters.component.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { ConnectorType } from '../../../../core/model/configuration'; import { Subject, debounceTime, take, takeUntil } from 'rxjs'; @@ -67,7 +67,7 @@ export type DialogListFilters = ExtractFormControlTyping; templateUrl: './dialogs-list-filters.component.html', styleUrl: './dialogs-list-filters.component.scss' }) -export class DialogsListFiltersComponent implements OnInit { +export class DialogsListFiltersComponent implements OnInit, OnDestroy { private readonly destroy$: Subject = new Subject(); private lastEmittedValue: Partial | null = null; @@ -109,8 +109,9 @@ export class DialogsListFiltersComponent implements OnInit { this.lastEmittedValue = { ...this.form.value }; } - this.form.valueChanges.pipe(debounceTime(800), takeUntil(this.destroy$)).subscribe(() => { + this.form.valueChanges.pipe(debounceTime(500), takeUntil(this.destroy$)).subscribe(() => { this.submitFiltersChange(); + this.persisteDisplayTests(); }); } @@ -143,10 +144,15 @@ export class DialogsListFiltersComponent implements OnInit { submitFiltersChange(): void { const formValue = this.form.value; - if (JSON.stringify(formValue) !== JSON.stringify(this.lastEmittedValue)) { - this.onFilter.emit(formValue); - this.lastEmittedValue = { ...formValue }; - } + this.onFilter.emit(formValue); + } + + persisteDisplayTests(): void { + const displayTests = this.getFormControl('displayTests')?.value; + this.botSharedService.session_storage = { + ...this.botSharedService.session_storage, + ...{ dialogs: { ...this.botSharedService.session_storage?.dialogs, displayTests } } + }; } resetControl(ctrl: FormControl, input?: HTMLInputElement): void { diff --git a/bot/admin/web/src/app/analytics/dialogs/dialogs-list/dialogs-list.component.ts b/bot/admin/web/src/app/analytics/dialogs/dialogs-list/dialogs-list.component.ts index 28001c0089..71862b0443 100644 --- a/bot/admin/web/src/app/analytics/dialogs/dialogs-list/dialogs-list.component.ts +++ b/bot/admin/web/src/app/analytics/dialogs/dialogs-list/dialogs-list.component.ts @@ -73,6 +73,10 @@ export class DialogsListComponent implements OnInit, OnChanges, OnDestroy { } ngOnInit() { + if (this.botSharedService.session_storage?.dialogs?.displayTests) { + this.filters.displayTests = this.botSharedService.session_storage.dialogs.displayTests; + } + this.state.configurationChange.pipe(takeUntil(this.destroy$)).subscribe(() => { this.refresh(); }); diff --git a/bot/admin/web/src/app/rag/rag-settings/models/engines-configurations.ts b/bot/admin/web/src/app/rag/rag-settings/models/engines-configurations.ts index 925980ad0e..9beab4b51a 100644 --- a/bot/admin/web/src/app/rag/rag-settings/models/engines-configurations.ts +++ b/bot/admin/web/src/app/rag/rag-settings/models/engines-configurations.ts @@ -27,35 +27,209 @@ import { PromptDefinitionFormatter } from '../../../shared/model/ai-settings'; -export const QuestionCondensingDefaultPrompt: string = `Given a chat history and the latest user question which might reference context in the chat history, formulate a standalone question which can be understood without the chat history. Do NOT answer the question, just reformulate it if needed and otherwise return it as is.`; +export const QuestionCondensingDefaultPrompt: string = `You are a helpful assistant that reformulates questions. + +You are given: +- The conversation history between the user and the assistant +- The most recent user question + +Your task: +- Reformulate the user’s latest question into a clear, standalone query. +- Incorporate relevant context from the conversation history. +- Do NOT answer the question. +- If the history does not provide additional context, keep the question as is. + +Return only the reformulated question.`; export const QuestionAnsweringDefaultPrompt: string = `# TOCK (The Open Conversation Kit) chatbot -## General context +## General Context You are a chatbot designed to provide short conversational messages in response to user queries. +Your job is to surface the right information from provided context. + +### Forbidden Topics + +- **Out of Scope**: + - Topics unrelated to the business domain (e.g., personal life advice, unrelated industries). + - Requests for unsupported features (e.g., "How do I integrate with [UnsupportedTool]?"). + +- **Toxic/Offensive Content**: + - Hate speech, harassment, or discriminatory language. + - Illegal activities or unethical requests (e.g., "How do I bypass security protocols?"). + +- **Personal/Private Matters**: + - User-specific data (e.g., personal identification, private conversations). + - Internal or confidential company information (e.g., unreleased product details). + +- **Regulated Topics**: + - Medical, legal, or financial advice (e.g., "What’s the best treatment for [condition]?"). + - Speculative or unverified claims (e.g., "Is [Product X] better than competitors?"). + +### Answer Style + +- **Tone**: neutral, kind, “you” address, light humor when appropriate. +- **Language**: Introduce technical jargon only when strictly necessary and briefly define it. +- **Structure**: Use short sentences, bold or bullet points for key ideas, headings to separate the main sections, and fenced \`code\` blocks for examples. Only include absolute links in your answers. +- **Style**: Direct and technical tone, with **bold** for important concepts. +- **Formatting**: Mandatory Markdown, with line breaks for readability. +- **Examples**: Include a concrete example (code block or CLI command) for each feature. + +### Guidelines + +1. If the question is unclear, politely request rephrasing. +2. If the docs lack or don’t cover the answer, reply with \`"status": "not_found_in_context"\`. +3. Conclude with: + - “Does this help?” + - Offer to continue on the same topic, switch topics, or contact support. + +### Verification Steps + +Before responding, ensure: + +- The documentation actually addresses the question. +- Your answer is consistent with the docs. +- If you include a link in your response, make sure it is an absolute link; otherwise, do not include it. + +## Technical Instructions: + +You must respond STRICTLY in valid JSON format (no extra text, no explanations). +Use only the following context and the rules below to respond the question. + +### Rules for JSON output: + +- If the answer is found in the context: + - "status": "found_in_context" + - "answer": the best possible answer in {{ locale }} + - "display_answer": "true" + - "redirection_intent": null -## Guidelines +- If the answer is NOT found in the context: + - "status": "not_found_in_context" + - "answer": + - The "answer" must not be a generic refusal. Instead, generate a helpful and intelligent response: + - If a similar or related element exists in the context (e.g., another product, service, or regulation with a close name, date, or wording), suggest it naturally in the answer. + - If no similar element exists, politely acknowledge the lack of information while encouraging clarification or rephrasing. + - Always ensure the response is phrased in a natural and user-friendly way, rather than a dry "not found in context". + - "display_answer": "true" + - "redirection_intent": null -Incorporate any relevant details from the provided context into your answers, ensuring they are directly related to the user's query. +- If the question is forbidden or offensive: + - "status": "out_of_scope" + - "answer": + - Generate a polite response explaining why a reponse can't be done. + - "topic": "Out of scope or offensive question" + - "display_answer": "true" + - "redirection_intent": null -## Style and format +- If the question is small talk: + Only to conversational rituals such as greetings (e.g., “hello”, “hi”) and farewells or leave-takings (e.g., “goodbye”, “see you”), you may ignore the context and generate a natural small-talk response in the "answer". + - "status": "small_talk" + - "topic": "greetings" + - "display_answer": "true" + - "redirection_intent": null -Your tone is empathetic, informative and polite. +### Confidence score: -## Additional instructions +Gives a confidence score between 0 and 1 on the relevance of the answer provided to the user's question: -Use the following pieces of retrieved context to answer the question. -If you dont know the answer, answer (exactly) with "{{no_answer}}". -Answer in {{locale}}. +- "confidence_score": -## Context +### Users question understanding: -{{context}} +Explain in one sentence what you understood from the user's question: -## Question +- "understanding": "" + +### Context usage tracing requirements (MANDATORY): + +- You MUST include **every** chunk from the input context in the "context_usage" array, in the same order they appear. **No chunk may be omitted**. +- If explicit chunk identifiers are present in the context, use them. +- For each chunk object: + - "chunk": "" + - "used_in_response": + - "true" if the chunk contributed + - "false" if the chunk didn't contributed + - "sentences": [""] — leave empty \`[]\` if none. + - "reason": "null" if the chunk contributed; otherwise a concise explanation of why this chunk is not relevant to the question (e.g., "general background only", "different product", "no data for the asked period", etc.). +- If there are zero chunks in the context, return \`"context": []\`. + +### Topic Identification & Suggestion Rules (MANDATORY): + +#### Rules for Topic Assignment + +- If the question explicitly matches a predefined topic, use: + - \`"topic": ""\` + - \`"suggested_topics": []\` + +- If the question does not match any predefined topic, use the \`unknown\` topic and provide 1 relevant and concise new topic suggestion in "suggested_topics": + - \`"topic": "unknown"\` + - \`"suggested_topics": [""]\` + +#### Predefined topics (use EXACT spelling, no variations): + +- \`Concepts and Definitions\` +- \`Processes and Methods\` +- \`Tools and Technologies\` +- \`Rules and Regulations\` +- \`Examples and Use Cases\` +- \`Resources and References\` + +## Context: + +{{ context }} + +## User question + +You are given the conversation history between a user and an assistant: + +- analyze the conversation history to understand the context and the user’s intent +- use this context to correctly interpret the user’s final question +- answer only the final user question below in a relevant and contextualized way + +Conversation history: +{{ chat_history }} + +User’s final question: +{{ question }} + +## Output format (JSON only): + +Return your response in the following format: + +\`\`\`json +{ + "status": "found_in_context" | "not_found_in_context" | "small_talk" | "out_of_scope", + "answer": "", + "display_answer": true | false, + "confidence_score": "", + "topic": "" | "greetings" | "Out of scope or offensive question" | "unknown", + "suggested_topics": ["", ""], + "understanding": "", + "redirection_intent": null, + "context_usage": [ + { + "chunk": "1", + "sentences": ["SENTENCE_1", "SENTENCE_2"], + "used_in_response": true | false, + "reason": null + }, + { + "chunk": "2", + "sentences": [], + "used_in_response": true | false, + "reason": "General description; no details related to the question." + }, + { + "chunk": "3", + "sentences": ["SENTENCE_X"], + "used_in_response": true | false, + "reason": null + } + ] +} +\`\`\` -{{question}} `; export const QuestionCondensing_prompt: ProvidersConfigurationParam[] = [ diff --git a/bot/admin/web/src/app/rag/rag-settings/models/rag-settings.ts b/bot/admin/web/src/app/rag/rag-settings/models/rag-settings.ts index 52b4dbb9fc..c29537b2bb 100644 --- a/bot/admin/web/src/app/rag/rag-settings/models/rag-settings.ts +++ b/bot/admin/web/src/app/rag/rag-settings/models/rag-settings.ts @@ -24,9 +24,6 @@ export interface RagSettings { debugEnabled: boolean; - noAnswerSentence: string; - noAnswerStoryId: string | null; - questionCondensingLlmSetting: llmSetting; questionCondensingPrompt: PromptDefinition; maxMessagesFromHistory: number; @@ -40,6 +37,4 @@ export interface RagSettings { indexName: string; maxDocumentsRetrieved: number; - - documentsRequired: boolean; } diff --git a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.html b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.html index 2ecc1b9a2c..7faf77dbdd 100644 --- a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.html +++ b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.html @@ -408,98 +408,6 @@

Rag settings

/> - -
- - - Don't allow undocumented answers - - -
- - - - - - Conversation flow - -
-
- - - -
-
- - - - - - - - - - {{ option.name }}  (disabled) - - -
diff --git a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.spec.ts b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.spec.ts index 75cebbc001..4a8dca8248 100644 --- a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.spec.ts +++ b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.spec.ts @@ -18,8 +18,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NbToastrService } from '@nebular/theme'; import { of } from 'rxjs'; -import { BotService } from '../../bot/bot-service'; -import { StoryDefinitionConfigurationSummary } from '../../bot/model/story'; import { RestService } from '../../core-nlp/rest/rest.service'; import { StateService } from '../../core-nlp/state.service'; import { BotConfigurationService } from '../../core/bot-configuration.service'; @@ -28,22 +26,6 @@ import { RagSettings } from './models'; import { RagSettingsComponent } from './rag-settings.component'; -const stories = [ - { - _id: '123456789abcdefghijkl', - storyId: 'teststory', - botId: 'new_assistant', - intent: { - name: 'testintent' - }, - currentType: 'simple', - name: 'Test story', - category: 'faq', - description: '', - lastEdited: '2023-07-31T14:48:21.291Z' - } as unknown as StoryDefinitionConfigurationSummary -]; - const settings = { id: 'abcdefghijkl123456789', namespace: 'app', @@ -64,9 +46,7 @@ const settings = { embeddingModelName: 'text-embedding-ada-002', embeddingApiKey: 'Embedding OpenAI API Key', embeddingApiVersion: '2023-03-15-preview' - }, - noAnswerSentence: 'No answer sentence', - noAnswerStoryId: 'null' + } } as unknown as RagSettings; describe('RagSettingsComponent', () => { @@ -77,12 +57,6 @@ describe('RagSettingsComponent', () => { await TestBed.configureTestingModule({ declarations: [RagSettingsComponent], providers: [ - { - provide: BotService, - useValue: { - searchStories: () => of(stories) - } - }, { provide: StateService, useValue: { @@ -118,10 +92,6 @@ describe('RagSettingsComponent', () => { expect(component).toBeTruthy(); }); - it('should load stories', () => { - expect(component.availableStories).toEqual(stories); - }); - it('should load settings', () => { expect(component.settingsBackup).toEqual(settings); @@ -130,7 +100,7 @@ describe('RagSettingsComponent', () => { delete cleanedSettings['botId']; const cleanedFormValue = deepCopy(component.form.getRawValue()); - delete cleanedFormValue.params.apiKey; + delete cleanedFormValue.questionAnsweringLlmSetting.apiKey; expect(cleanedFormValue as unknown).toEqual(cleanedSettings as unknown); }); diff --git a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.ts b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.ts index 0a7f41f8a3..12d8cdee99 100644 --- a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.ts +++ b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.ts @@ -16,11 +16,9 @@ import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { debounceTime, forkJoin, Observable, of, Subject, take, takeUntil, pairwise } from 'rxjs'; +import { debounceTime, forkJoin, Observable, Subject, takeUntil, pairwise } from 'rxjs'; import { NbDialogRef, NbDialogService, NbToastrService, NbWindowService } from '@nebular/theme'; -import { BotService } from '../../bot/bot-service'; -import { StoryDefinitionConfiguration, StorySearchQuery } from '../../bot/model/story'; import { RestService } from '../../core-nlp/rest/rest.service'; import { StateService } from '../../core-nlp/state.service'; import { EnginesConfigurations, QuestionCondensing_prompt, QuestionAnswering_prompt } from './models/engines-configurations'; @@ -47,16 +45,11 @@ interface RagSettingsForm { debugEnabled: FormControl; - noAnswerSentence: FormControl; - noAnswerStoryId: FormControl; - indexSessionId: FormControl; indexName: FormControl; maxDocumentsRetrieved: FormControl; - documentsRequired: FormControl; - questionCondensingLlmProvider: FormControl; questionCondensingLlmSetting: FormGroup; questionCondensingPrompt: FormGroup; @@ -89,10 +82,6 @@ export class RagSettingsComponent implements OnInit, OnDestroy { questionAnswering_prompt = QuestionAnswering_prompt; - availableStories: StoryDefinitionConfiguration[]; - - filteredStories$: Observable; - settingsBackup: RagSettings; isSubmitted: boolean = false; @@ -103,7 +92,6 @@ export class RagSettingsComponent implements OnInit, OnDestroy { @ViewChild('importModal') importModal: TemplateRef; constructor( - private botService: BotService, private state: StateService, private rest: RestService, private toastrService: NbToastrService, @@ -176,14 +164,9 @@ export class RagSettingsComponent implements OnInit, OnDestroy { this.configurations = confs; if (confs.length) { - forkJoin([this.getStoriesLoader(), this.getRagSettingsLoader()]).subscribe((res) => { - this.availableStories = res[0]; - - const settings = res[1]; + forkJoin([this.getRagSettingsLoader()]).subscribe((res) => { + const settings = res[0]; if (settings?.id) { - if (!settings.noAnswerStoryId) { - settings.noAnswerStoryId = null; - } this.settingsBackup = deepCopy(settings); setTimeout(() => { this.initForm(settings); @@ -209,13 +192,9 @@ export class RagSettingsComponent implements OnInit, OnDestroy { debugEnabled: new FormControl({ value: undefined, disabled: !this.canRagBeActivated() }), - noAnswerSentence: new FormControl(undefined, [Validators.required]), - noAnswerStoryId: new FormControl(undefined), - indexSessionId: new FormControl(undefined), indexName: new FormControl(undefined), - documentsRequired: new FormControl(undefined), maxDocumentsRetrieved: new FormControl(undefined), questionCondensingLlmProvider: new FormControl(undefined, [Validators.required]), @@ -256,21 +235,10 @@ export class RagSettingsComponent implements OnInit, OnDestroy { return this.form.get('maxMessagesFromHistory') as FormControl; } - get noAnswerSentence(): FormControl { - return this.form.get('noAnswerSentence') as FormControl; - } - get noAnswerStoryId(): FormControl { - return this.form.get('noAnswerStoryId') as FormControl; - } - get indexSessionId(): FormControl { return this.form.get('indexSessionId') as FormControl; } - get documentsRequired(): FormControl { - return this.form.get('documentsRequired') as FormControl; - } - get maxDocumentsRetrieved(): FormControl { return this.form.get('maxDocumentsRetrieved') as FormControl; } @@ -283,11 +251,6 @@ export class RagSettingsComponent implements OnInit, OnDestroy { return this.isSubmitted ? this.form.valid : this.form.dirty; } - get getCurrentStoryLabel(): string { - const currentStory = this.availableStories?.find((story) => story.storyId === this.noAnswerStoryId.value); - return currentStory?.name || ''; - } - accordionItemsExpandedState: Map; isAccordionItemsExpanded(itemName: string): boolean { @@ -393,7 +356,6 @@ export class RagSettingsComponent implements OnInit, OnDestroy { setFormDefaultValues(): void { this.form.patchValue({ - documentsRequired: false, debugEnabled: false, maxMessagesFromHistory: 5, maxDocumentsRetrieved: 4 @@ -455,66 +417,6 @@ export class RagSettingsComponent implements OnInit, OnDestroy { return EnginesConfigurations[AiEngineSettingKeyName.emSetting].find((e) => e.key === this.emProvider.value); } - private getStoriesLoader(): Observable { - return this.botService - .getStories( - new StorySearchQuery( - this.state.currentApplication.namespace, - this.state.currentApplication.name, - this.state.currentLocale, - 0, - 10000, - undefined, - undefined, - false - ) - ) - .pipe(take(1)); - } - - isStoryEnabled(story: StoryDefinitionConfiguration): boolean { - for (let i = 0; i < story.features.length; i++) { - if (!story.features[i].enabled && !story.features[i].switchToStoryId && !story.features[i].endWithStoryId) { - return false; - } - } - return true; - } - - storySelectedChange(storyId: string): void { - this.noAnswerStoryId.patchValue(storyId); - this.form.markAsDirty(); - } - - onStoryChange(value: string): void { - if (value?.trim() == '') { - this.removeNoAnswerStoryId(); - } - } - - removeNoAnswerStoryId(): void { - this.noAnswerStoryId.patchValue(null); - this.form.markAsDirty(); - } - - filterStoriesList(e: string): void { - this.filteredStories$ = of(this.availableStories.filter((optionValue) => optionValue.name.toLowerCase().includes(e.toLowerCase()))); - } - - storyInputFocus(): void { - this.filteredStories$ = of(this.availableStories); - } - - storyInputBlur(e: FocusEvent): void { - setTimeout(() => { - // timeout needed to avoid reseting input and filtered stories when clicking on autocomplete suggestions (which fires blur event) - const target: HTMLInputElement = e.target as HTMLInputElement; - target.value = this.getCurrentStoryLabel; - - this.filteredStories$ = of(this.availableStories); - }, 100); - } - cancel(): void { this.initForm(this.settingsBackup); } @@ -529,14 +431,10 @@ export class RagSettingsComponent implements OnInit, OnDestroy { delete formValue['emProvider']; formValue.namespace = this.state.currentApplication.namespace; formValue.botId = this.state.currentApplication.name; - formValue.noAnswerStoryId = this.noAnswerStoryId.value === 'null' ? null : this.noAnswerStoryId.value; const url = `/gen-ai/bots/${this.state.currentApplication.name}/configuration/rag`; this.rest.post(url, formValue, null, null, true).subscribe({ next: (ragSettings: RagSettings) => { - if (!ragSettings.noAnswerStoryId) { - ragSettings.noAnswerStoryId = null; - } this.settingsBackup = ragSettings; this.indexName.reset(); diff --git a/bot/admin/web/src/app/shared/bot-shared.service.ts b/bot/admin/web/src/app/shared/bot-shared.service.ts index 5fae05656e..dcf933be48 100644 --- a/bot/admin/web/src/app/shared/bot-shared.service.ts +++ b/bot/admin/web/src/app/shared/bot-shared.service.ts @@ -25,6 +25,7 @@ import { AdminConfiguration } from './model/conf'; export interface TockSimpleSessionStorage { test: { debug: boolean; sourceWithContent?: boolean }; + dialogs: { displayTests: boolean }; } @Injectable({ diff --git a/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.html b/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.html index a9a3c82749..865f88d462 100644 --- a/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.html +++ b/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.html @@ -14,10 +14,55 @@ ~ limitations under the License. --> -
- {{ message.text }} DEBUG +
+
+ Status | {{ getStatusLabel() }} +
+ +
+ Topic | {{ message.data.answer?.topic }} +
+ +
+ Suggested topics | + + + {{ suggestion }} + + +
+ +
+ Redirection intent | {{ message.data.answer?.redirection_intent }} +
+ +
+ Confidence | {{ message.data.answer?.confidence_score }} +
diff --git a/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.scss b/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.scss index b0b18a6758..d698b164ed 100644 --- a/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.scss +++ b/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.scss @@ -14,26 +14,56 @@ * limitations under the License. */ -@import '@nebular/theme/styles/theming'; -@import '@nebular/theme/styles/themes'; - :host { display: flex; - justify-content: center; .debug { - cursor: pointer; + width: 100%; + + margin-top: 0.5em; + margin-bottom: 0.5em; + + .tag { + display: inline-flex; + width: fit-content; + padding: 0.2rem 0.5rem; + border-radius: 10rem; + background-color: var(--border-basic-color-3); + font-size: 0.7rem; + line-height: 1rem; + white-space: nowrap; + cursor: pointer; - color: var(--chat-message-sender-text-color); - border-bottom: 1px dashed var(--chat-message-sender-text-color); + &.status-success { + background-color: var(--color-success-500); + color: white; + } + &.status-warning { + background-color: var(--color-warning-500); + color: white; + } + &.status-info { + background-color: var(--color-info-500); + color: white; + } + &.status-danger { + background-color: var(--color-danger-500); + color: white; + } - height: 11px; - margin-bottom: 10px; + &.topic { + background-color: var(--color-success-300); + color: var(--color-success-600); + } + &.suggested-topics { + background-color: var(--color-warning-300); + color: var(--color-warning-600); + } - span { - background: var(--card-background-color); - padding: 0 5px; - border-radius: 0.5rem; + &.confidence { + background-color: var(--color-default-300); + color: var(--text-basic-color); + } } } } diff --git a/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.ts b/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.ts index 12a45039e6..b6adb66ada 100644 --- a/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.ts +++ b/bot/admin/web/src/app/shared/components/chat-ui/chat-ui-message/chat-ui-message-debug/chat-ui-message-debug.component.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { NbDialogService } from '@nebular/theme'; -import { Debug } from '../../../../model/dialog-data'; +import { Debug, RagAnswerStatus, RagAnswerStatusLabels } from '../../../../model/dialog-data'; import { DebugViewerDialogComponent } from '../../../debug-viewer-dialog/debug-viewer-dialog.component'; +import { getContrastYIQ, getInterpolatedColor } from '../../../../utils'; @Component({ selector: 'tock-chat-ui-message-debug', @@ -35,4 +36,42 @@ export class ChatUiMessageDebugComponent { } }); } + + getStatusClassName(): string { + const status = this.message.data.answer?.status; + if (!status) { + return ''; + } + + switch (status.toLowerCase()) { + case RagAnswerStatus.FOUND_IN_CONTEXT: + return 'status-success'; + case RagAnswerStatus.NOT_FOUND_IN_CONTEXT: + return 'status-warning'; + case RagAnswerStatus.SMALL_TALK: + return 'status-info'; + case RagAnswerStatus.OUT_OF_SCOPE: + return 'status-danger'; + default: + return ''; + } + } + + getStatusLabel(): string { + const status = this.message.data.answer?.status; + if (status) { + return RagAnswerStatusLabels[status.toLowerCase()] || status.replace(/_/g, ' ').replace(/^(.)|\s+(.)/g, (c) => c.toUpperCase()); + } + return ''; + } + + getConfidenceBgColor(): { bg: string; fg: string } { + const score = this.message.data.answer?.confidence_score; + if (score != null) { + const bg = getInterpolatedColor(score); + const fg = getContrastYIQ(bg); + return { bg, fg }; + } + return { bg: '', fg: '' }; + } } diff --git a/bot/admin/web/src/app/shared/components/debug-viewer-dialog/debug-viewer-dialog.component.html b/bot/admin/web/src/app/shared/components/debug-viewer-dialog/debug-viewer-dialog.component.html index 0f933db352..72d0d3f817 100644 --- a/bot/admin/web/src/app/shared/components/debug-viewer-dialog/debug-viewer-dialog.component.html +++ b/bot/admin/web/src/app/shared/components/debug-viewer-dialog/debug-viewer-dialog.component.html @@ -29,6 +29,9 @@ - + diff --git a/bot/admin/web/src/app/shared/components/json-iterator/json-iterator.component.html b/bot/admin/web/src/app/shared/components/json-iterator/json-iterator.component.html index f06b3718a4..ad6f8bb052 100644 --- a/bot/admin/web/src/app/shared/components/json-iterator/json-iterator.component.html +++ b/bot/admin/web/src/app/shared/components/json-iterator/json-iterator.component.html @@ -33,31 +33,43 @@ *ngIf="parentKey" (click)="switchDeployed()" class="parent-title" - [ngClass]="{ isPrimitive: isPrimitive(data), ellipsis: !isPrimitive(data), pointer: !isPrimitive(data) }" + [ngClass]="{ pointer: isExpandable() }" > - - - {{ parentKey }} : +
+
+ + +
+
+ {{ parentKey }} : -
+      "{{ data }}"
+      
 "{{ data }}"
- {{ data | json }} + > + + {{ data | json }} +
+
- + { if (evt.type === 'expand' && !this.isDeployed) { this.isDeployed = true; @@ -46,15 +51,55 @@ export class JsonIteratorComponent implements OnDestroy { }); } + ngAfterViewInit() { + this.width = this.elementRef.nativeElement.offsetWidth; + this.resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + this.width = Math.floor(entry.contentRect.width); + this.cdr.detectChanges(); + } + }); + this.resizeObserver.observe(this.elementRef.nativeElement); + this.cdr.detectChanges(); + } + + customSort = (a: { key: string }, b: { key: string }) => { + const order = this.customOrder; + const aIndex = order.indexOf(a.key); + const bIndex = order.indexOf(b.key); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + return a.key.localeCompare(b.key); + }; + + isExpandable(): boolean { + if (!this.isPrimitive(this.data)) return true; + + const fontSize = 12; + const maxChars = Math.floor(this.width / (0.6 * fontSize)); + const fullTextLength = String(this.parentKey).length + String(this.data).length + 6; // +6 for parent name, space, colon, space and quotes around the value + + return fullTextLength >= maxChars; + } + switchDeployed() { this.isDeployed = !this.isDeployed; + this.cdr.detectChanges(); } expandAll() { this.jsonIteratorService.expandAll(); + this.cdr.detectChanges(); } ngOnDestroy(): void { + this.resizeObserver.disconnect(); this.destroy.next(true); this.destroy.complete(); } diff --git a/bot/admin/web/src/app/shared/model/dialog-data.ts b/bot/admin/web/src/app/shared/model/dialog-data.ts index 428ebb1962..b8253f54a6 100644 --- a/bot/admin/web/src/app/shared/model/dialog-data.ts +++ b/bot/admin/web/src/app/shared/model/dialog-data.ts @@ -315,6 +315,20 @@ export class Debug extends BotMessage { } } +export enum RagAnswerStatus { + FOUND_IN_CONTEXT = 'found_in_context', + NOT_FOUND_IN_CONTEXT = 'not_found_in_context', + SMALL_TALK = 'small_talk', + OUT_OF_SCOPE = 'out_of_scope' +} + +export const RagAnswerStatusLabels: Record = { + [RagAnswerStatus.FOUND_IN_CONTEXT]: 'Found in context', + [RagAnswerStatus.NOT_FOUND_IN_CONTEXT]: 'Not found in context', + [RagAnswerStatus.SMALL_TALK]: 'Small talk', + [RagAnswerStatus.OUT_OF_SCOPE]: 'Out of scope' +}; + export class SentenceElement { constructor( public connectorType: ConnectorType, diff --git a/bot/admin/web/src/app/shared/utils/utils.ts b/bot/admin/web/src/app/shared/utils/utils.ts index 7a4164040b..d886dd6c5d 100644 --- a/bot/admin/web/src/app/shared/utils/utils.ts +++ b/bot/admin/web/src/app/shared/utils/utils.ts @@ -174,6 +174,42 @@ export function shadeColor(hexcolor: string, amount: number) { return '#' + RR + GG + BB; } +/** + * Interpolates between two hex colors based on a value between 0 and 1. + * + * @param {number} value - A value between 0 and 1. Clamped to this range if outside. + * @param {string} [colorStart="#acbef4"] - The starting hex color (e.g., "#acbef4"). + * @param {string} [colorEnd="#3366ff"] - The ending hex color (e.g., "#3366ff"). + * @returns {string} The interpolated hex color as a string. + * + * @example + * // Returns a color halfway between #acbef4 and #3366ff + * const interpolatedColor = getInterpolatedColor(0.5, "#acbef4", "#3366ff"); + */ +export function getInterpolatedColor(value: number, colorStart: string = '#acbef4', colorEnd: string = '#3366ff'): string { + // Clamp value between 0 and 1 + const clampedValue = Math.min(1, Math.max(0, value)); + + // Parse hex colors to RGB + const parseHex = (hex: string) => { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return { r, g, b }; + }; + + const start = parseHex(colorStart); + const end = parseHex(colorEnd); + + // Interpolate each channel + const r = Math.round(start.r + (end.r - start.r) * clampedValue); + const g = Math.round(start.g + (end.g - start.g) * clampedValue); + const b = Math.round(start.b + (end.b - start.b) * clampedValue); + + // Convert back to hex + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; +} + export async function copyToClipboard(text: string): Promise { if (navigator.clipboard) { await navigator.clipboard.writeText(text); diff --git a/bot/admin/web/src/app/theme/components/footer/footer.component.html b/bot/admin/web/src/app/theme/components/footer/footer.component.html index 3ab9c982cc..5454d1ed41 100644 --- a/bot/admin/web/src/app/theme/components/footer/footer.component.html +++ b/bot/admin/web/src/app/theme/components/footer/footer.component.html @@ -15,29 +15,42 @@ -->
-
- {{ (configuration | async).globalMessage}} +
+ {{ globalMessage }}
+
Tock v{{ tock_info.build_version }} - -   - -   - + > +   + +   +
diff --git a/bot/admin/web/src/app/theme/components/footer/footer.component.ts b/bot/admin/web/src/app/theme/components/footer/footer.component.ts index 18fb558f55..9862c5605c 100644 --- a/bot/admin/web/src/app/theme/components/footer/footer.component.ts +++ b/bot/admin/web/src/app/theme/components/footer/footer.component.ts @@ -14,20 +14,32 @@ * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { tock_info } from '../../../../environments/manifest'; import { BotSharedService } from '../../../shared/bot-shared.service'; import { AdminConfiguration } from '../../../shared/model/conf'; +import { take } from 'rxjs'; @Component({ selector: 'tock-footer', templateUrl: './footer.component.html', styleUrls: ['./footer.component.scss'] }) -export class FooterComponent { +export class FooterComponent implements OnInit { constructor(private botSharedService: BotSharedService) {} tock_info = tock_info; - configuration = this.botSharedService.getConfiguration() + globalMessage = ''; + + ngOnInit() { + this.botSharedService + .getConfiguration() + .pipe(take(1)) + .subscribe((conf: AdminConfiguration) => { + if (conf.globalMessage) { + this.globalMessage = conf.globalMessage; + } + }); + } } diff --git a/bot/admin/web/src/app/theme/components/header/header.component.html b/bot/admin/web/src/app/theme/components/header/header.component.html index b76bd7871c..4256d73059 100644 --- a/bot/admin/web/src/app/theme/components/header/header.component.html +++ b/bot/admin/web/src/app/theme/components/header/header.component.html @@ -103,6 +103,16 @@ + +