Skip to content

Commit 6004fc4

Browse files
committed
[DERCBOT-1609] Structuring the LLM response
1 parent 144c7c2 commit 6004fc4

File tree

10 files changed

+429
-229
lines changed

10 files changed

+429
-229
lines changed

bot/admin/web/src/app/rag/rag-settings/models/engines-configurations.ts

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,102 @@ import {
2727
PromptDefinitionFormatter
2828
} from '../../../shared/model/ai-settings';
2929

30-
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.`;
30+
export const QuestionCondensingDefaultPrompt: string = `You are a helpful assistant that reformulates questions.
3131
32-
export const QuestionAnsweringDefaultPrompt: string = `# TOCK (The Open Conversation Kit) chatbot
33-
34-
## General context
35-
36-
You are a chatbot designed to provide short conversational messages in response to user queries.
37-
38-
## Guidelines
39-
40-
Incorporate any relevant details from the provided context into your answers, ensuring they are directly related to the user's query.
32+
You are given:
33+
- The conversation history between the user and the assistant
34+
- The most recent user question
4135
42-
## Style and format
36+
Your task:
37+
- Reformulate the user’s latest question into a clear, standalone query.
38+
- Incorporate relevant context from the conversation history.
39+
- Do NOT answer the question.
40+
- If the history does not provide additional context, keep the question as is.
4341
44-
Your tone is empathetic, informative and polite.
42+
Return only the reformulated question.`;
4543

46-
## Additional instructions
47-
48-
Use the following pieces of retrieved context to answer the question.
49-
If you dont know the answer, answer (exactly) with "{{no_answer}}".
50-
Answer in {{locale}}.
51-
52-
## Context
53-
54-
{{context}}
55-
56-
## Question
44+
export const QuestionAnsweringDefaultPrompt: string = `# TOCK (The Open Conversation Kit) chatbot
5745
58-
{{question}}
46+
## Instructions:
47+
You must answer STRICTLY in valid JSON format (no extra text, no explanations).
48+
Use only the following context and the rules below to answer the question.
49+
50+
### Rules for JSON output:
51+
52+
- If the answer is found in the context:
53+
- "status": "found_in_context"
54+
55+
- If the answer is NOT found in the context:
56+
- "status": "not_found_in_context"
57+
- "answer":
58+
- The "answer" must not be a generic refusal. Instead, generate a helpful and intelligent response:
59+
- 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.
60+
- If no similar element exists, politely acknowledge the lack of information while encouraging clarification or rephrasing.
61+
- Always ensure the response is phrased in a natural and user-friendly way, rather than a dry "not found in context".
62+
63+
- If the question matches a special case defined below:
64+
- "status": "<the corresponding case code>"
65+
66+
And for all cases (MANDATORY):
67+
- "answer": "<the best possible answer in {{ locale }}>"
68+
- "topic": "<exactly ONE topic chosen STRICTLY from the predefined list below. If no exact match is possible, set 'unknown'>"
69+
- "suggested_topics": ["<zero or more free-form suggestions if topic is unknown>"]
70+
71+
Exception: 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". In this case:
72+
- "status": "small_talk"
73+
- "topic": "<e.g., greetings>"
74+
- "suggested_topics": []
75+
- "context": []
76+
77+
### Context tracing requirements (MANDATORY):
78+
- You MUST include **every** chunk from the input context in the "context" array, in the same order they appear. **No chunk may be omitted**.
79+
- If explicit chunk identifiers are present in the context, use them; otherwise assign sequential numbers starting at 1.
80+
- For each chunk object:
81+
- "chunk": "<chunk_identifier_or_sequential_number>"
82+
- "sentences": ["<verbatim sentence(s) from this chunk used to answer the question>"] — leave empty \`[]\` if none.
83+
- "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.).
84+
- If there are zero chunks in the context, return \`"context": []\`.
85+
86+
### Predefined list of topics (use EXACT spelling, no variations):
87+
88+
## Context:
89+
{{ context }}
90+
91+
## Conversation history
92+
{{ chat_history }}
93+
94+
## User question
95+
{{ question }}
96+
97+
## Output format (JSON only):
98+
Return your response in the following format:
99+
100+
{
101+
"status": "found_on_context" | "not_in_context" | "small_talk",
102+
"answer": "TEXTUAL_ANSWER",
103+
"topic": "EXACT_TOPIC_FROM_LIST_OR_UNKNOWN",
104+
"suggested_topics": [
105+
"SUGGESTED_TOPIC_1",
106+
"SUGGESTED_TOPIC_2"
107+
],
108+
"context": [
109+
{
110+
"chunk": "1",
111+
"sentences": ["SENTENCE_1", "SENTENCE_2"],
112+
"reason": null
113+
},
114+
{
115+
"chunk": "2",
116+
"sentences": [],
117+
"reason": "General description; no details related to the question."
118+
},
119+
{
120+
"chunk": "3",
121+
"sentences": ["SENTENCE_X"],
122+
"reason": null
123+
}
124+
]
125+
}
59126
`;
60127

61128
export const QuestionCondensing_prompt: ProvidersConfigurationParam[] = [

bot/engine/src/main/kotlin/admin/bot/rag/BotRAGConfiguration.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ data class BotRAGConfiguration(
3636
val llmSetting: LLMSetting? = null,
3737
val emSetting: EMSetting,
3838
val indexSessionId: String? = null,
39+
@Deprecated("Replaced by LLM answer status")
3940
val noAnswerSentence: String,
4041
val noAnswerStoryId: String? = null,
4142
val documentsRequired: Boolean = true,

bot/engine/src/main/kotlin/engine/config/RAGAnswerHandler.kt

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ import ai.tock.bot.engine.action.SendSentenceWithFootnotes
3131
import ai.tock.bot.engine.dialog.Dialog
3232
import ai.tock.bot.engine.user.PlayerType
3333
import ai.tock.genai.orchestratorclient.requests.*
34+
import ai.tock.genai.orchestratorclient.responses.LLMAnswer
3435
import ai.tock.genai.orchestratorclient.responses.ObservabilityInfo
3536
import ai.tock.genai.orchestratorclient.responses.RAGResponse
36-
import ai.tock.genai.orchestratorclient.responses.TextWithFootnotes
3737
import ai.tock.genai.orchestratorclient.retrofit.GenAIOrchestratorBusinessError
3838
import ai.tock.genai.orchestratorclient.retrofit.GenAIOrchestratorValidationError
3939
import ai.tock.genai.orchestratorclient.services.RAGService
@@ -60,7 +60,7 @@ object RAGAnswerHandler : AbstractProactiveAnswerHandler {
6060
BotRepository.saveMetric(createMetric(MetricType.STORY_HANDLED))
6161

6262
// Call RAG Api - Gen AI Orchestrator
63-
val (answer, debug, noAnswerStory, observabilityInfo) = rag(this)
63+
val (answer, footnotes, debug, noAnswerStory, observabilityInfo) = rag(this)
6464

6565
// Add debug data if available and if debugging is enabled
6666
if (debug != null) {
@@ -75,14 +75,18 @@ object RAGAnswerHandler : AbstractProactiveAnswerHandler {
7575
val modifiedObservabilityInfo = observabilityInfo?.let { updateObservabilityInfo(this, it) }
7676

7777
send(
78-
SendSentenceWithFootnotes(
79-
botId, connectorId, userId, text = answer.text, footnotes = answer.footnotes.map {
78+
action = SendSentenceWithFootnotes(
79+
playerId = botId,
80+
applicationId = connectorId,
81+
recipientId = userId,
82+
text = answer.answer,
83+
footnotes = footnotes?.map {
8084
Footnote(
8185
it.identifier, it.title, it.url,
8286
if(action.metadata.sourceWithContent) it.content else null,
8387
it.score
8488
)
85-
}.toMutableList(),
89+
}?.toMutableList() ?: mutableListOf<Footnote>(),
8690
// modifiedObservabilityInfo includes the public langfuse URL if filled.
8791
metadata = ActionMetadata(isGenAiRagAnswer = true, observabilityInfo = modifiedObservabilityInfo)
8892
)
@@ -116,13 +120,13 @@ object RAGAnswerHandler : AbstractProactiveAnswerHandler {
116120
private fun ragStoryRedirection(botBus: BotBus, response: RAGResponse?): StoryDefinition? {
117121
return with(botBus) {
118122
botDefinition.ragConfiguration?.let { ragConfig ->
119-
if (response?.answer?.text.equals(ragConfig.noAnswerSentence, ignoreCase = true)) {
123+
if (response?.answer?.status.equals("not_found_in_context", ignoreCase = true)) {
120124
// Save no answer metric
121125
saveRagMetric(IndicatorValues.NO_ANSWER)
122126

123127
// Switch to no answer story if configured
124128
if (!ragConfig.noAnswerStoryId.isNullOrBlank()) {
125-
logger.info { "The RAG response is equal to the configured no-answer sentence, so switch to the no-answer story." }
129+
logger.info { "Switch to the no-answer RAG story." }
126130
getNoAnswerRAGStory(ragConfig)
127131
} else null
128132
} else {
@@ -221,7 +225,7 @@ object RAGAnswerHandler : AbstractProactiveAnswerHandler {
221225
)
222226

223227
// Handle RAG response
224-
return RAGResult(response?.answer, response?.debug, ragStoryRedirection(this, response), response?.observabilityInfo)
228+
return RAGResult(response?.answer, response?.footnotes, response?.debug, ragStoryRedirection(this, response), response?.observabilityInfo)
225229
} catch (exc: Exception) {
226230
logger.error { exc }
227231
// Save failure metric
@@ -232,7 +236,7 @@ object RAGAnswerHandler : AbstractProactiveAnswerHandler {
232236
RAGResult(noAnswerStory = getNoAnswerRAGStory(ragConfiguration))
233237
}
234238
else RAGResult(
235-
answer = TextWithFootnotes(text = technicalErrorMessage),
239+
answer = LLMAnswer(status="error", answer = technicalErrorMessage),
236240
debug = when(exc) {
237241
is GenAIOrchestratorBusinessError -> RAGError(exc.message, exc.error)
238242
is GenAIOrchestratorValidationError -> RAGError(exc.message, exc.detail)
@@ -282,7 +286,8 @@ object RAGAnswerHandler : AbstractProactiveAnswerHandler {
282286
* Aggregation of RAG answer, debug and the no answer Story.
283287
*/
284288
data class RAGResult(
285-
val answer: TextWithFootnotes? = null,
289+
val answer: LLMAnswer? = null,
290+
val footnotes: List<ai.tock.genai.orchestratorclient.responses.Footnote>? = null,
286291
val debug: Any? = null,
287292
val noAnswerStory: StoryDefinition? = null,
288293
val observabilityInfo: ObservabilityInfo? = null,

gen-ai/orchestrator-client/src/main/kotlin/ai/tock/genai/orchestratorclient/responses/RAGResponse.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
package ai.tock.genai.orchestratorclient.responses
1818

1919
data class RAGResponse(
20-
val answer: TextWithFootnotes,
20+
val answer: LLMAnswer,
21+
val footnotes: List<Footnote> = emptyList(),
2122
val debug: Any? = null,
2223
val observabilityInfo: ObservabilityInfo? = null,
2324
)

gen-ai/orchestrator-client/src/main/kotlin/ai/tock/genai/orchestratorclient/responses/models.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,18 @@
1717
package ai.tock.genai.orchestratorclient.responses
1818

1919

20-
data class TextWithFootnotes(
21-
val text: String,
22-
val footnotes: List<Footnote> = emptyList(),
20+
data class ChunkSentences(
21+
val chunk: String? = null,
22+
val sentences: List<String>? = emptyList(),
23+
val reason: String? = null,
24+
)
25+
26+
data class LLMAnswer(
27+
val status: String,
28+
val answer: String,
29+
val topic: String? = null,
30+
val suggestedTopics: List<String>? = null,
31+
val context: List<ChunkSentences>? = null,
2332
)
2433

2534
data class Footnote(

gen-ai/orchestrator-core/src/main/kotlin/ai/tock/genai/orchestratorcore/models/llm/OllamaLLMSetting.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,3 @@ data class OllamaLLMSetting<T>(
2828
}
2929
}
3030

31-
// TODO MASS : Check Compile + TU (car dernier commit)

gen-ai/orchestrator-server/src/main/python/server/src/gen_ai_orchestrator/models/rag/rag_models.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,55 @@ class Footnote(Source):
5252

5353
identifier: str = Field(description='Footnote identifier', examples=['1'])
5454

55+
class ChunkInfos(BaseModel):
56+
"""A model representing information about a chunk used in the RAG context."""
5557

56-
class TextWithFootnotes(BaseModel):
57-
"""Text with its footnotes. Used for RAG response"""
58-
59-
text: str = Field(
60-
description='Text with footnotes used to list outside sources',
61-
examples=['This is page content [1], and this is more content [2]'],
58+
chunk: Optional[str] = Field(
59+
description='Unique identifier of the chunk.',
60+
examples=['cd6d8221-ba9f-44da-86ee-0e25a3c9a5c7'],
61+
default=None
62+
)
63+
sentences: Optional[List[str]] = Field(
64+
description='List of verbatim sentences from the chunk that were used by the LLM.',
65+
default=None
6266
)
63-
footnotes: set[Footnote] = Field(description='Set of footnotes')
67+
reason: Optional[str] = Field(
68+
description='Reason why the chunk was not used (e.g., irrelevant, general background).',
69+
default=None
70+
)
71+
72+
73+
class LLMAnswer(BaseModel):
74+
"""
75+
A model representing the structured answer generated by the LLM
76+
in response to a user query, based on the provided RAG context.
77+
"""
6478

79+
status: Optional[str] = Field(
80+
description="The status of the answer generation. "
81+
"Possible values: 'found_in_context', 'not_found_in_context', 'small_talk', "
82+
"or other case-specific codes.",
83+
default=None
84+
)
85+
answer: Optional[str] = Field(
86+
description="The textual answer generated by the LLM, in the user's locale.",
87+
default=None
88+
)
89+
topic: Optional[str] = Field(
90+
description="The main topic assigned to the answer. Must be one of the predefined list "
91+
"of topics, or 'unknown' if no match is possible.",
92+
default=None
93+
)
94+
suggested_topics: Optional[List[str]] = Field(
95+
description="A list of suggested alternative or related topics, "
96+
"used when the main topic is 'unknown'.",
97+
default=None
98+
)
99+
context: Optional[List[ChunkInfos]] = Field(
100+
description="The list of chunks from the context that contributed to or were considered "
101+
"in the LLM's answer. Each entry contains identifiers, sentences, and reasons.",
102+
default=None
103+
)
65104

66105
@unique
67106
class ChatMessageType(str, Enum):
@@ -154,4 +193,4 @@ class RAGDebugData(QADebugData):
154193
'Question: Hello, how to plan a trip to Morocco ?. Answer in French.'
155194
],
156195
)
157-
answer: str = Field(description='The RAG answer.')
196+
answer: LLMAnswer = Field(description='The RAG answer.')

gen-ai/orchestrator-server/src/main/python/server/src/gen_ai_orchestrator/routers/responses/responses.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@
2525
ErrorInfo,
2626
)
2727
from gen_ai_orchestrator.models.llm.llm_provider import LLMProvider
28-
from gen_ai_orchestrator.models.rag.rag_models import Source, TextWithFootnotes
28+
from gen_ai_orchestrator.models.rag.rag_models import Source, LLMAnswer, Footnote
2929
from gen_ai_orchestrator.models.observability.observability_provider import ObservabilityProvider
30-
from gen_ai_orchestrator.models.rag.rag_models import TextWithFootnotes
3130
from gen_ai_orchestrator.models.vector_stores.vectore_store_provider import VectorStoreProvider
3231

3332

@@ -122,9 +121,10 @@ class ObservabilityInfo(BaseModel):
122121
class RAGResponse(BaseModel):
123122
"""The RAG response model"""
124123

125-
answer: TextWithFootnotes = Field(
126-
description='The RAG answer, with outside sources.'
124+
answer: Optional[LLMAnswer] = Field(
125+
description='The RAG answer'
127126
)
127+
footnotes: set[Footnote] = Field(description='Set of footnotes')
128128
debug: Optional[Any] = Field(
129129
description='Debug data',
130130
examples=[{'action': 'retrieve', 'result': 'OK', 'errors': []}],

0 commit comments

Comments
 (0)