Skip to content

Commit 8105c5c

Browse files
feat: Make suggest next questions configurable (#275)
--------- Co-authored-by: Marcus Schiesser <[email protected]>
1 parent c16deed commit 8105c5c

File tree

12 files changed

+182
-320
lines changed

12 files changed

+182
-320
lines changed

.changeset/cyan-buttons-clean.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-llama": patch
3+
---
4+
5+
Add env config for next questions feature

helpers/env-variables.ts

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -487,33 +487,30 @@ It\\'s cute animal.
487487
};
488488

489489
const getTemplateEnvs = (template?: TemplateType): EnvVar[] => {
490-
if (template === "multiagent") {
491-
return [
492-
{
493-
name: "MESSAGE_QUEUE_PORT",
494-
},
495-
{
496-
name: "CONTROL_PLANE_PORT",
497-
},
498-
{
499-
name: "HUMAN_CONSUMER_PORT",
500-
},
501-
{
502-
name: "AGENT_QUERY_ENGINE_PORT",
503-
value: "8003",
504-
},
505-
{
506-
name: "AGENT_QUERY_ENGINE_DESCRIPTION",
507-
value: "Query information from the provided data",
508-
},
509-
{
510-
name: "AGENT_DUMMY_PORT",
511-
value: "8004",
512-
},
513-
];
514-
} else {
515-
return [];
490+
const nextQuestionEnvs: EnvVar[] = [
491+
{
492+
name: "NEXT_QUESTION_PROMPT",
493+
description: `Customize prompt to generate the next question suggestions based on the conversation history.
494+
Disable this prompt to disable the next question suggestions feature.`,
495+
value: `"You're a helpful assistant! Your task is to suggest the next question that user might ask.
496+
Here is the conversation history
497+
---------------------
498+
{conversation}
499+
---------------------
500+
Given the conversation history, please give me 3 questions that you might ask next!
501+
Your answer should be wrapped in three sticks which follows the following format:
502+
\`\`\`
503+
<question 1>
504+
<question 2>
505+
<question 3>
506+
\`\`\`"`,
507+
},
508+
];
509+
510+
if (template === "multiagent" || template === "streaming") {
511+
return nextQuestionEnvs;
516512
}
513+
return [];
517514
};
518515

519516
const getObservabilityEnvs = (

helpers/python.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ export const installPythonTemplate = async ({
395395
cwd: path.join(compPath, "settings", "python"),
396396
});
397397

398+
// Copy services
399+
if (template == "streaming" || template == "multiagent") {
400+
await copy("**", path.join(root, "app", "api", "services"), {
401+
cwd: path.join(compPath, "services", "python"),
402+
});
403+
}
404+
398405
if (template === "streaming") {
399406
// For the streaming template only:
400407
// Select and copy engine code based on data sources and tools

templates/components/llamaindex/typescript/streaming/suggestion.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,20 @@
11
import { ChatMessage, Settings } from "llamaindex";
22

3-
const NEXT_QUESTION_PROMPT_TEMPLATE = `You're a helpful assistant! Your task is to suggest the next question that user might ask.
4-
Here is the conversation history
5-
---------------------
6-
$conversation
7-
---------------------
8-
Given the conversation history, please give me $number_of_questions questions that you might ask next!
9-
Your answer should be wrapped in three sticks which follows the following format:
10-
\`\`\`
11-
<question 1>
12-
<question 2>\`\`\`
13-
`;
14-
const N_QUESTIONS_TO_GENERATE = 3;
15-
16-
export async function generateNextQuestions(
17-
conversation: ChatMessage[],
18-
numberOfQuestions: number = N_QUESTIONS_TO_GENERATE,
19-
) {
3+
export async function generateNextQuestions(conversation: ChatMessage[]) {
204
const llm = Settings.llm;
5+
const NEXT_QUESTION_PROMPT = process.env.NEXT_QUESTION_PROMPT;
6+
if (!NEXT_QUESTION_PROMPT) {
7+
return [];
8+
}
219

2210
// Format conversation
2311
const conversationText = conversation
2412
.map((message) => `${message.role}: ${message.content}`)
2513
.join("\n");
26-
const message = NEXT_QUESTION_PROMPT_TEMPLATE.replace(
27-
"$conversation",
14+
const message = NEXT_QUESTION_PROMPT.replace(
15+
"{conversation}",
2816
conversationText,
29-
).replace("$number_of_questions", numberOfQuestions.toString());
17+
);
3018

3119
try {
3220
const response = await llm.complete({ prompt: message });
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import logging
2+
import os
3+
import re
4+
from typing import List, Optional
5+
6+
from app.api.routers.models import Message
7+
from llama_index.core.prompts import PromptTemplate
8+
from llama_index.core.settings import Settings
9+
10+
logger = logging.getLogger("uvicorn")
11+
12+
13+
class NextQuestionSuggestion:
14+
"""
15+
Suggest the next questions that user might ask based on the conversation history
16+
Disable this feature by removing the NEXT_QUESTION_PROMPT environment variable
17+
"""
18+
19+
@classmethod
20+
def get_configured_prompt(cls) -> Optional[str]:
21+
prompt = os.getenv("NEXT_QUESTION_PROMPT", None)
22+
if not prompt:
23+
return None
24+
return PromptTemplate(prompt)
25+
26+
@classmethod
27+
async def suggest_next_questions_all_messages(
28+
cls,
29+
messages: List[Message],
30+
) -> Optional[List[str]]:
31+
"""
32+
Suggest the next questions that user might ask based on the conversation history
33+
Return None if suggestion is disabled or there is an error
34+
"""
35+
prompt_template = cls.get_configured_prompt()
36+
if not prompt_template:
37+
return None
38+
39+
try:
40+
# Reduce the cost by only using the last two messages
41+
last_user_message = None
42+
last_assistant_message = None
43+
for message in reversed(messages):
44+
if message.role == "user":
45+
last_user_message = f"User: {message.content}"
46+
elif message.role == "assistant":
47+
last_assistant_message = f"Assistant: {message.content}"
48+
if last_user_message and last_assistant_message:
49+
break
50+
conversation: str = f"{last_user_message}\n{last_assistant_message}"
51+
52+
# Call the LLM and parse questions from the output
53+
prompt = prompt_template.format(conversation=conversation)
54+
output = await Settings.llm.acomplete(prompt)
55+
questions = cls._extract_questions(output.text)
56+
57+
return questions
58+
except Exception as e:
59+
logger.error(f"Error when generating next question: {e}")
60+
return None
61+
62+
@classmethod
63+
def _extract_questions(cls, text: str) -> List[str]:
64+
content_match = re.search(r"```(.*?)```", text, re.DOTALL)
65+
content = content_match.group(1) if content_match else ""
66+
return content.strip().split("\n")
67+
68+
@classmethod
69+
async def suggest_next_questions(
70+
cls,
71+
chat_history: List[Message],
72+
response: str,
73+
) -> List[str]:
74+
"""
75+
Suggest the next questions that user might ask based on the chat history and the last response
76+
"""
77+
messages = chat_history + [Message(role="assistant", content=response)]
78+
return await cls.suggest_next_questions_all_messages(messages)

templates/types/multiagent/fastapi/app/api/routers/vercel_response.py

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
from asyncio import Task
21
import json
32
import logging
4-
from typing import AsyncGenerator
3+
from asyncio import Task
4+
from typing import AsyncGenerator, List
55

66
from aiostream import stream
7+
from app.agents.single import AgentRunEvent, AgentRunResult
8+
from app.api.routers.models import ChatData, Message
9+
from app.api.services.suggestion import NextQuestionSuggestion
710
from fastapi import Request
811
from fastapi.responses import StreamingResponse
912

10-
from app.api.routers.models import ChatData
11-
from app.agents.single import AgentRunEvent, AgentRunResult
12-
1313
logger = logging.getLogger("uvicorn")
1414

1515

@@ -57,26 +57,35 @@ async def content_generator(
5757
# Yield the text response
5858
async def _chat_response_generator():
5959
result = await task
60+
final_response = ""
6061

6162
if isinstance(result, AgentRunResult):
6263
for token in result.response.message.content:
63-
yield VercelStreamResponse.convert_text(token)
64+
final_response += token
65+
yield cls.convert_text(token)
6466

6567
if isinstance(result, AsyncGenerator):
6668
async for token in result:
67-
yield VercelStreamResponse.convert_text(token.delta)
69+
final_response += token.delta
70+
yield cls.convert_text(token.delta)
71+
72+
# Generate next questions if next question prompt is configured
73+
question_data = await cls._generate_next_questions(
74+
chat_data.messages, final_response
75+
)
76+
if question_data:
77+
yield cls.convert_data(question_data)
6878

69-
# TODO: stream NextQuestionSuggestion
7079
# TODO: stream sources
7180

7281
# Yield the events from the event handler
7382
async def _event_generator():
7483
async for event in events():
75-
event_response = _event_to_response(event)
84+
event_response = cls._event_to_response(event)
7685
if verbose:
7786
logger.debug(event_response)
7887
if event_response is not None:
79-
yield VercelStreamResponse.convert_data(event_response)
88+
yield cls.convert_data(event_response)
8089

8190
combine = stream.merge(_chat_response_generator(), _event_generator())
8291

@@ -85,16 +94,28 @@ async def _event_generator():
8594
if not is_stream_started:
8695
is_stream_started = True
8796
# Stream a blank message to start the stream
88-
yield VercelStreamResponse.convert_text("")
97+
yield cls.convert_text("")
8998

9099
async for output in streamer:
91100
yield output
92101
if await request.is_disconnected():
93102
break
94103

95-
96-
def _event_to_response(event: AgentRunEvent) -> dict:
97-
return {
98-
"type": "agent",
99-
"data": {"agent": event.name, "text": event.msg},
100-
}
104+
@staticmethod
105+
def _event_to_response(event: AgentRunEvent) -> dict:
106+
return {
107+
"type": "agent",
108+
"data": {"agent": event.name, "text": event.msg},
109+
}
110+
111+
@staticmethod
112+
async def _generate_next_questions(chat_history: List[Message], response: str):
113+
questions = await NextQuestionSuggestion.suggest_next_questions(
114+
chat_history, response
115+
)
116+
if questions:
117+
return {
118+
"type": "suggested_questions",
119+
"data": questions,
120+
}
121+
return None

templates/types/multiagent/fastapi/app/api/services/suggestion.py

Lines changed: 0 additions & 60 deletions
This file was deleted.

0 commit comments

Comments
 (0)