Skip to content

Commit d2a59ff

Browse files
committed
ิcreate package specifying agent
1 parent e1e45a2 commit d2a59ff

File tree

5 files changed

+197
-103
lines changed

5 files changed

+197
-103
lines changed

src/fastapi_app/postgres_searcher.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def build_filter_clause(self, filters) -> tuple[str, str]:
3535
return f"WHERE {filter_clause}", f"AND {filter_clause}"
3636
return "", ""
3737

38-
async def search(
38+
async def hybrid_search(
3939
self,
4040
query_text: str | None,
4141
query_vector: list[float] | list,
@@ -281,4 +281,33 @@ async def search_and_embed(
281281
if not enable_text_search:
282282
query_text = None
283283

284-
return await self.search(query_text, vector, top, filters)
284+
return await self.hybrid_search(query_text, vector, top, filters)
285+
286+
async def simple_sql_search(
287+
self,
288+
filters: list[dict]
289+
) -> list[Item]:
290+
"""
291+
Search items by simple SQL query with filters.
292+
"""
293+
filter_clause_where, _ = self.build_filter_clause(filters)
294+
sql = f"""
295+
SELECT id FROM packages
296+
{filter_clause_where}
297+
LIMIT 1
298+
"""
299+
300+
async with self.async_session_maker() as session:
301+
results = (
302+
await session.execute(
303+
text(sql).columns(id=Integer)
304+
)
305+
).fetchall()
306+
307+
# Convert results to Item models
308+
items = []
309+
for result in results:
310+
item_id = result.id
311+
item = await session.execute(select(Item).where(Item.id == item_id))
312+
items.append(item.scalar())
313+
return items

src/fastapi_app/prompts/answer.txt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
You are a woman name Jib (จิ๊บ), a sales consultant from HDmall which you need to convince the customers to buy some products from our store name HDmall, Please answer in casual and friendly manner. Don't recommend any product yet on the first conversation before the customer asking. You can use Emoji to make the conversation more friendly if you want.
22
Represent youself as จิ๊บ and the customer as คุณลูกค้า when the conversation is in Thai language.
3-
Represent youself as Jib and the customer as you when the conversation is in English language.
4-
When the customer asks about any packages, please make sure to provide brand, price , URL and location every time.
5-
If customer wants to talk with admin, please provide the URL link in the datasource.
6-
Note that some packages may have additional cost.
3+
Represent youself as Jib and the customer as you when the conversation is in English or Non-Thai language.
4+
Answer the customer's question in the same language as the customer's question.
5+
It's not important whether the customer is male or female, you are woman named Jib, please end with "ค่ะ" when you chat with customer in Thai language.
6+
If the customer want to buy a product or book a service, or having an strong intent of interested in some package, respond "QISCUS_INTEGRATION_TO_CX" to handover to customer service.
7+
When the customer asks about any packages, please make sure to provide brand, price ,URL which in a new line, location with google maps link every time.
78
If the user is asking a question regarding location, proximity, or area, query relevant document from the source and ask where the user is at. please try to suggest services or answers closest to the location the user is asking as much as possible.
8-
Answer ONLY with the facts listed in the list of sources below. If there isn't enough information below, say sorry you don't know. Do not generate answers that don't use the sources below. If asking a clarifying question to the user would help, ask the question.
9-
Answer in well-structured plain text, not a markdown.
9+
Answer ONLY with the facts listed in the list of sources below. If there isn't enough information below, say sorry you don't know (in users' language). Do not generate answers that don't use the sources below. If asking a clarifying question to the user would help, ask the question.
10+
Since this is a text-based conversation in a chatroom, do not use markdown or any similar rich formatting. Also, don't wrap URL in a parentheses.
11+
Note that some packages may have additional cost.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Please specify the exact URL or package name from past messages only if the user's message directly references a known package. Do not attempt to identify packages based on general inquiries or price-related requests. If the user's message does not clearly match a previously mentioned package, respond accordingly without specifying a package name.

src/fastapi_app/query_rewriter.py

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
)
77

88

9-
def build_search_function() -> list[ChatCompletionToolParam]:
9+
def build_hybrid_search_function() -> list[ChatCompletionToolParam]:
1010
return [
1111
{
1212
"type": "function",
@@ -26,39 +26,21 @@ def build_search_function() -> list[ChatCompletionToolParam]:
2626
"properties": {
2727
"comparison_operator": {
2828
"type": "string",
29-
"description": "Operator to compare the column value, either '>', '<', '>=', '<=', '='", # noqa
29+
"description": "Operator to compare the column value, either '>', '<', '>=', '<=', '='",
3030
},
3131
"value": {
3232
"type": "number",
3333
"description": "Value to compare against, e.g. 30",
3434
},
3535
},
3636
},
37-
"url_filter": {
38-
"type": "object",
39-
"description": "Filter search results based on url of the package. The url is package specific.",
40-
"properties": {
41-
"comparison_operator": {
42-
"type": "string",
43-
"description": "Operator to compare the column value, either '=' or '!='",
44-
},
45-
"value": {
46-
"type": "string",
47-
"description": """
48-
The package URL to compare against.
49-
Don't pass anything if you can't specify the exact URL from user query.
50-
""",
51-
},
52-
},
53-
},
5437
},
55-
"required": ["search_query", "url_filter"],
38+
"required": ["search_query"],
5639
},
5740
},
5841
}
5942
]
6043

61-
6244
def extract_search_arguments(chat_completion: ChatCompletion):
6345
response_message = chat_completion.choices[0].message
6446
search_query = None
@@ -70,7 +52,6 @@ def extract_search_arguments(chat_completion: ChatCompletion):
7052
function = tool.function
7153
if function.name == "search_database":
7254
arg = json.loads(function.arguments)
73-
print(arg)
7455
search_query = arg.get("search_query")
7556
if "price_filter" in arg and arg["price_filter"]:
7657
price_filter = arg["price_filter"]
@@ -81,16 +62,70 @@ def extract_search_arguments(chat_completion: ChatCompletion):
8162
"value": price_filter["value"],
8263
}
8364
)
84-
if "url_filter" in arg and arg["url_filter"]:
85-
url_filter = arg["url_filter"]
86-
if url_filter["value"] != "https://hdmall.co.th":
87-
filters.append(
88-
{
89-
"column": "url",
90-
"comparison_operator": url_filter["comparison_operator"],
91-
"value": url_filter["value"],
92-
}
93-
)
9465
elif query_text := response_message.content:
9566
search_query = query_text.strip()
9667
return search_query, filters
68+
69+
70+
def build_specify_package_function() -> list[ChatCompletionToolParam]:
71+
return [
72+
{
73+
"type": "function",
74+
"function": {
75+
"name": "specify_package",
76+
"description": """
77+
Specify the exact URL or package name from past messages if they are relevant to the most recent user's message.
78+
This tool is intended to find specific packages previously mentioned and should not be used for general inquiries or price-based requests.
79+
""",
80+
"parameters": {
81+
"type": "object",
82+
"properties": {
83+
"url": {
84+
"type": "string",
85+
"description": """
86+
The exact URL of the package from past messages,
87+
e.g. 'https://hdmall.co.th/dental-clinics/xray-for-orthodontics-1-csdc'
88+
"""
89+
},
90+
"package_name": {
91+
"type": "string",
92+
"description": """
93+
The exact package name from past messages,
94+
always contains the package name and the hospital name,
95+
e.g. 'เอกซเรย์สำหรับการจัดฟัน ที่ CSDC'
96+
"""
97+
}
98+
},
99+
"required": [],
100+
},
101+
},
102+
}
103+
]
104+
105+
def handle_specify_package_function_call(chat_completion: ChatCompletion):
106+
response_message = chat_completion.choices[0].message
107+
filters = []
108+
if response_message.tool_calls:
109+
for tool in response_message.tool_calls:
110+
if tool.type == "function" and tool.function.name == "specify_package":
111+
args = json.loads(tool.function.arguments)
112+
url = args.get("url")
113+
package_name = args.get("package_name")
114+
if url:
115+
filters.append(
116+
{
117+
"column": "url",
118+
"comparison_operator": "=",
119+
"value": url,
120+
}
121+
)
122+
if package_name:
123+
filters.append(
124+
{
125+
"column": "package_name",
126+
"comparison_operator": "=",
127+
"value": package_name,
128+
}
129+
)
130+
return filters
131+

src/fastapi_app/rag_advanced.py

Lines changed: 90 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313

1414
from .api_models import ThoughtStep
1515
from .postgres_searcher import PostgresSearcher
16-
from .query_rewriter import build_search_function, extract_search_arguments
16+
from .query_rewriter import (
17+
build_hybrid_search_function,
18+
extract_search_arguments,
19+
build_specify_package_function,
20+
handle_specify_package_function_call
21+
)
1722

1823

1924
class AdvancedRAGChat:
@@ -31,6 +36,7 @@ def __init__(
3136
self.chat_deployment = chat_deployment
3237
self.chat_token_limit = get_token_limit(chat_model, default_to_minimum=True)
3338
current_dir = pathlib.Path(__file__).parent
39+
self.specify_package_prompt_template = open(current_dir / "prompts/specify_package.txt").read()
3440
self.query_prompt_template = open(current_dir / "prompts/query.txt").read()
3541
self.answer_prompt_template = open(current_dir / "prompts/answer.txt").read()
3642

@@ -47,40 +53,95 @@ async def run(
4753
vector_search = overrides.get("retrieval_mode") in ["vectors", "hybrid", None]
4854
top = overrides.get("top", 3)
4955

56+
# Generate a prompt to specify the package if the user is referring to a specific package
57+
specify_package_messages = copy.deepcopy(messages)
58+
specify_package_messages.insert(0, {"role": "system", "content": self.specify_package_prompt_template})
59+
specify_package_token_limit = 300
5060

51-
# Generate an optimized keyword search query based on the chat history and the last question
52-
query_messages = copy.deepcopy(messages)
53-
query_messages.insert(0, {"role": "system", "content": self.query_prompt_template})
54-
query_response_token_limit = 500
55-
56-
chat_completion: ChatCompletion = await self.openai_chat_client.chat.completions.create(
57-
messages=query_messages, # type: ignore
58-
# Azure OpenAI takes the deployment name as the model name
61+
specify_package_chat_completion: ChatCompletion = await self.openai_chat_client.chat.completions.create(
62+
messages=specify_package_messages,
5963
model=self.chat_deployment if self.chat_deployment else self.chat_model,
60-
temperature=0.0, # Minimize creativity for search query generation
61-
max_tokens=query_response_token_limit, # Setting too low risks malformed JSON, too high risks performance
64+
temperature=0.0,
65+
max_tokens=specify_package_token_limit,
6266
n=1,
63-
tools=build_search_function(),
64-
tool_choice="auto",
67+
tools=build_specify_package_function()
6568
)
6669

67-
query_text, filters = extract_search_arguments(chat_completion)
70+
specify_package_filters = handle_specify_package_function_call(specify_package_chat_completion)
6871

69-
# Retrieve relevant items from the database with the GPT optimized query
70-
results = await self.searcher.search_and_embed(
71-
query_text,
72-
top=top,
73-
enable_vector_search=vector_search,
74-
enable_text_search=text_search,
75-
filters=filters,
76-
)
77-
78-
# Check if the url_filter is used to determine the context to send to the LLM
79-
if any(f['column'] == 'url' and f['value'] != '' for f in filters):
80-
sources_content = [f"[{(item.id)}]:{item.to_str_for_narrow_rag()}\n\n" for item in results] # all details
72+
if specify_package_filters:
73+
# Pass specify_package_filters to simple SQL search function
74+
results = await self.searcher.simple_sql_search(filters=specify_package_filters)
75+
sources_content = [f"[{(item.id)}]:{item.to_str_for_narrow_rag()}\n\n" for item in results]
76+
77+
thought_steps = [
78+
ThoughtStep(
79+
title="Prompt to specify package",
80+
description=[str(message) for message in specify_package_messages],
81+
props={"model": self.chat_model, "deployment": self.chat_deployment} if self.chat_deployment else {"model": self.chat_model}
82+
),
83+
ThoughtStep(
84+
title="Specified package filters",
85+
description=specify_package_filters,
86+
props={}
87+
),
88+
ThoughtStep(
89+
title="SQL search results",
90+
description=[result.to_dict() for result in results],
91+
props={}
92+
)
93+
]
8194
else:
82-
sources_content = [f"[{(item.id)}]:{item.to_str_for_broad_rag()}\n\n" for item in results] # important details
83-
95+
# Generate an optimized keyword search query based on the chat history and the last question
96+
query_messages = copy.deepcopy(messages)
97+
query_messages.insert(0, {"role": "system", "content": self.query_prompt_template})
98+
query_response_token_limit = 500
99+
100+
query_chat_completion: ChatCompletion = await self.openai_chat_client.chat.completions.create(
101+
messages=query_messages,
102+
model=self.chat_deployment if self.chat_deployment else self.chat_model,
103+
temperature=0.0,
104+
max_tokens=query_response_token_limit,
105+
n=1,
106+
tools=build_hybrid_search_function(),
107+
tool_choice="auto",
108+
)
109+
110+
query_text, filters = extract_search_arguments(query_chat_completion)
111+
112+
# Retrieve relevant items from the database with the GPT optimized query
113+
results = await self.searcher.search_and_embed(
114+
query_text,
115+
top=top,
116+
enable_vector_search=vector_search,
117+
enable_text_search=text_search,
118+
filters=filters,
119+
)
120+
121+
sources_content = [f"[{(item.id)}]:{item.to_str_for_broad_rag()}\n\n" for item in results]
122+
123+
thought_steps = [
124+
ThoughtStep(
125+
title="Prompt to generate search arguments",
126+
description=[str(message) for message in query_messages],
127+
props={"model": self.chat_model, "deployment": self.chat_deployment} if self.chat_deployment else {"model": self.chat_model}
128+
),
129+
ThoughtStep(
130+
title="Generated search arguments",
131+
description=query_text,
132+
props={"filters": filters}
133+
),
134+
ThoughtStep(
135+
title="Hybrid Search results",
136+
description=[result.to_dict() for result in results],
137+
props={
138+
"top": top,
139+
"vector_search": vector_search,
140+
"text_search": text_search
141+
}
142+
)
143+
]
144+
84145
content = "\n".join(sources_content)
85146

86147
# Build messages for the final chat completion
@@ -89,7 +150,6 @@ async def run(
89150
response_token_limit = 1024
90151

91152
chat_completion_response = await self.openai_chat_client.chat.completions.create(
92-
# Azure OpenAI takes the deployment name as the model name
93153
model=self.chat_deployment if self.chat_deployment else self.chat_model,
94154
messages=messages,
95155
temperature=overrides.get("temperature", 0.3),
@@ -101,39 +161,6 @@ async def run(
101161

102162
chat_resp["choices"][0]["context"] = {
103163
"data_points": {"text": sources_content},
104-
"thoughts": [
105-
ThoughtStep(
106-
title="Prompt to generate search arguments",
107-
description=[str(message) for message in query_messages],
108-
props=(
109-
{"model": self.chat_model, "deployment": self.chat_deployment}
110-
if self.chat_deployment
111-
else {"model": self.chat_model}
112-
),
113-
),
114-
ThoughtStep(
115-
title="Search using generated search arguments",
116-
description=query_text,
117-
props={
118-
"top": top,
119-
"vector_search": vector_search,
120-
"text_search": text_search,
121-
"filters": filters,
122-
},
123-
),
124-
ThoughtStep(
125-
title="Search results",
126-
description=[result.to_dict() for result in results],
127-
),
128-
ThoughtStep(
129-
title="Prompt to generate answer",
130-
description=[str(message) for message in messages],
131-
props=(
132-
{"model": self.chat_model, "deployment": self.chat_deployment}
133-
if self.chat_deployment
134-
else {"model": self.chat_model}
135-
),
136-
),
137-
],
164+
"thoughts": thought_steps
138165
}
139166
return chat_resp

0 commit comments

Comments
 (0)