Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(search): conforms api requests to chat protocol (#67) #82

Merged
merged 1 commit into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 13 additions & 22 deletions packages/chat-component/src/core/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,24 @@ export async function callHttpApi(
{ question, type, approach, overrides }: ChatRequestOptions,
{ method, url, stream }: ChatHttpOptions,
) {
const chatBody = JSON.stringify({
messages: [
{
user: question,
},
],
context: {
...overrides,
approach,
},
stream,
});
const askBody = JSON.stringify({
question,
context: {
...overrides,
approach,
},
stream: false,
});
const body = type === 'chat' ? chatBody : askBody;
return await fetch(`${url}/${type}`, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body,
body: JSON.stringify({
messages: [
{
content: question,
role: 'user',
},
],
context: {
...overrides,
approach,
},
stream: type === 'chat' ? stream : false,
}),
});
}

Expand Down
9 changes: 3 additions & 6 deletions packages/search/src/lib/approaches/approach.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Message, type HistoryMessage } from '../message.js';
import { type Message } from '../message.js';

export interface ApproachResponse {
choices: Array<{
Expand Down Expand Up @@ -42,11 +42,8 @@ export type ChatApproachContext = ApproachContext & {
};

export interface ChatApproach {
run(history: HistoryMessage[], context?: ChatApproachContext): Promise<ApproachResponse>;
runWithStreaming(
history: HistoryMessage[],
context?: ChatApproachContext,
): AsyncGenerator<ApproachResponseChunk, void>;
run(messages: Message[], context?: ChatApproachContext): Promise<ApproachResponse>;
runWithStreaming(messages: Message[], context?: ChatApproachContext): AsyncGenerator<ApproachResponseChunk, void>;
}

export interface AskApproach {
Expand Down
43 changes: 18 additions & 25 deletions packages/search/src/lib/approaches/chat-read-retrieve-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
type ApproachResponseChunk,
} from './approach.js';
import { ApproachBase } from './approach-base.js';
import { type HistoryMessage, type Message, messagesToString } from '../message.js';
import { type Message, messagesToString } from '../message.js';
import { MessageBuilder } from '../message-builder.js';
import { getTokenLimit } from '../tokens.js';

Expand Down Expand Up @@ -64,8 +64,8 @@ export class ChatReadRetrieveRead extends ApproachBase implements ChatApproach {
this.chatGptTokenLimit = getTokenLimit(chatGptModel);
}

async run(history: HistoryMessage[], context?: ChatApproachContext): Promise<ApproachResponse> {
const { completionRequest, dataPoints, thoughts } = await this.baseRun(history, context);
async run(messages: Message[], context?: ChatApproachContext): Promise<ApproachResponse> {
const { completionRequest, dataPoints, thoughts } = await this.baseRun(messages, context);
const openAiChat = await this.openai.getChat();
const chatCompletion = await openAiChat.completions.create(completionRequest);
const chatContent = chatCompletion.choices[0].message.content ?? '';
Expand All @@ -89,10 +89,10 @@ export class ChatReadRetrieveRead extends ApproachBase implements ChatApproach {
}

async *runWithStreaming(
history: HistoryMessage[],
messages: Message[],
context?: ChatApproachContext,
): AsyncGenerator<ApproachResponseChunk, void> {
const { completionRequest, dataPoints, thoughts } = await this.baseRun(history, context);
const { completionRequest, dataPoints, thoughts } = await this.baseRun(messages, context);
const openAiChat = await this.openai.getChat();
const chatCompletion = await openAiChat.completions.create({
...completionRequest,
Expand Down Expand Up @@ -122,16 +122,16 @@ export class ChatReadRetrieveRead extends ApproachBase implements ChatApproach {
}
}

private async baseRun(history: HistoryMessage[], context?: ChatApproachContext) {
const userQuery = 'Generate search query for: ' + history[history.length - 1].user;
private async baseRun(messages: Message[], context?: ChatApproachContext) {
const userQuery = 'Generate search query for: ' + messages[messages.length - 1].content;

// STEP 1: Generate an optimized keyword search query based on the chat history and the last question
// -----------------------------------------------------------------------

const messages = this.getMessagesFromHistory(
const initialMessages = this.getMessagesFromHistory(
QUERY_PROMPT_TEMPLATE,
this.chatGptModel,
history,
messages,
userQuery,
QUERY_PROMPT_FEW_SHOTS,
this.chatGptTokenLimit - userQuery.length,
Expand All @@ -140,7 +140,7 @@ export class ChatReadRetrieveRead extends ApproachBase implements ChatApproach {
const openAiChat = await this.openai.getChat();
const chatCompletion = await openAiChat.completions.create({
model: this.chatGptModel,
messages,
messages: initialMessages,
temperature: 0,
max_tokens: 32,
n: 1,
Expand All @@ -149,7 +149,7 @@ export class ChatReadRetrieveRead extends ApproachBase implements ChatApproach {
let queryText = chatCompletion.choices[0].message.content?.trim();
if (queryText === '0') {
// Use the last user input if we failed to generate a better query
queryText = history[history.length - 1].user;
queryText = messages[messages.length - 1].content;
}

// STEP 2: Retrieve relevant documents from the search index with the GPT optimized query
Expand Down Expand Up @@ -184,15 +184,15 @@ export class ChatReadRetrieveRead extends ApproachBase implements ChatApproach {
const finalMessages = this.getMessagesFromHistory(
systemMessage,
this.chatGptModel,
history,
messages,
// Model does not handle lengthy system messages well.
// Moving sources to latest user conversation to solve follow up questions prompt.
`${history[history.length - 1].user}\n\nSources:\n${content}`,
`${messages[messages.length - 1].content}\n\nSources:\n${content}`,
[],
this.chatGptTokenLimit,
);

const firstQuery = messagesToString(messages);
const firstQuery = messagesToString(initialMessages);
const secondQuery = messagesToString(finalMessages);
const thoughts = `Search query:
${query}
Expand All @@ -218,7 +218,7 @@ ${secondQuery}`.replaceAll('\n', '<br>');
private getMessagesFromHistory(
systemPrompt: string,
model: string,
history: HistoryMessage[],
history: Message[],
userContent: string,
fewShots: Message[] = [],
maxTokens = 4096,
Expand All @@ -238,18 +238,11 @@ ${secondQuery}`.replaceAll('\n', '<br>');
if (messageBuilder.tokens > maxTokens) {
break;
}
if (historyMessage.bot) {
messageBuilder.appendMessage('assistant', historyMessage.bot, appendIndex);
}
if (historyMessage.user) {
messageBuilder.appendMessage('user', historyMessage.user, appendIndex);
}
if (messageBuilder.tokens > maxTokens) {
break;
if (historyMessage.role === 'assistant' || historyMessage.role === 'user') {
messageBuilder.appendMessage(historyMessage.role, historyMessage.content, appendIndex);
}
}

const messages = messageBuilder.messages;
return messages.map((m) => ({ role: m.role, content: m.content }));
return messageBuilder.messages;
}
}
5 changes: 0 additions & 5 deletions packages/search/src/lib/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ export interface Message {
content: string;
}

export interface HistoryMessage {
bot?: string;
user?: string;
}

export function messageToString(message: Message): string {
return `${message.role}: ${message.content}`;
}
Expand Down
40 changes: 10 additions & 30 deletions packages/search/src/plugins/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ export const chatRequestSchema = {
items: {
type: 'object',
properties: {
bot: { type: 'string' },
user: { type: 'string' },
content: { type: 'string' },
// can be only: assistant, user, system or function
role: {
type: 'string',
enum: ['system', 'user', 'assistant', 'function'],
},
},
required: ['content', 'role'],
additionalProperties: false,
},
},
stream: { type: 'boolean' },
Expand All @@ -30,27 +36,6 @@ export const chatRequestSchema = {
required: ['messages'],
} as const;

export const askRequestSchema = {
$id: 'askRequest',
type: 'object',
properties: {
question: { type: 'string' },
stream: { type: 'boolean' },
context: {
type: 'object',
properties: {
approach: { type: 'string' },
},
additionalProperties: { type: 'string' },
},
session_state: {
type: 'object',
additionalProperties: { type: 'string' },
},
},
required: ['question'],
} as const;

export const messageSchema = {
$id: 'message',
type: 'object',
Expand Down Expand Up @@ -98,14 +83,9 @@ export const approachResponseSchema = {
required: ['choices', 'object'],
} as const;

export const schemas = [chatRequestSchema, askRequestSchema, messageSchema, approachResponseSchema];
export const schemas = [chatRequestSchema, messageSchema, approachResponseSchema];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the askRequestSchema being deleted? I'm not sure I understand this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it's not necessary anymore, both APIs use the same schema now that the chat protocol


export type SchemaTypes = [
typeof chatRequestSchema,
typeof askRequestSchema,
typeof messageSchema,
typeof approachResponseSchema,
];
export type SchemaTypes = [typeof chatRequestSchema, typeof messageSchema, typeof approachResponseSchema];

export default fp(async (fastify, _options): Promise<void> => {
for (const schema of schemas) {
Expand Down
8 changes: 4 additions & 4 deletions packages/search/src/routes/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const root: FastifyPluginAsync = async (_fastify, _options): Promise<void> => {
schema: {
description: 'Ask the bot a question',
tags: ['ask'],
body: { $ref: 'askRequest' },
body: { $ref: 'chatRequest' },
response: {
// 200: { $ref: 'approachResponse' },
400: { $ref: 'httpError' },
Expand All @@ -125,22 +125,22 @@ const root: FastifyPluginAsync = async (_fastify, _options): Promise<void> => {
return reply.badRequest(`Ask approach "${approach}" is unknown or not implemented.`);
}

const { question, context, stream } = request.body;
const { messages, context, stream } = request.body;
try {
if (stream) {
const buffer = new Readable();
// Dummy implementation needed
buffer._read = () => {};
reply.type('application/x-ndjson').send(buffer);

const chunks = await askApproach.runWithStreaming(question, (context as any) ?? {});
const chunks = await askApproach.runWithStreaming(messages[0].content, (context as any) ?? {});
for await (const chunk of chunks) {
buffer.push(JSON.stringify(chunk) + '\n');
}
// eslint-disable-next-line unicorn/no-null
buffer.push(null);
} else {
return await askApproach.run(question, (context as any) ?? {});
return await askApproach.run(messages[0].content, (context as any) ?? {});
}
} catch (_error: unknown) {
const error = _error as Error;
Expand Down
20 changes: 16 additions & 4 deletions packages/search/test.http
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ POST {{api_host}}/chat
Content-Type: application/json

{
"messages": [{ "user": "What happens if a rental doesn't fit the description?" }],
"messages": [{
"content": "What happens if a rental doesn't fit the description?",
"role": "user"
}],
"context": {
"approach":"rrr",
"retrieval_mode": "hybrid",
Expand All @@ -33,7 +36,10 @@ POST {{api_host}}/chat
Content-Type: application/json

{
"messages": [{ "user": "What happens if a rental doesn't fit the description?" }],
"messages": [{
"content": "What happens if a rental doesn't fit the description?",
"role": "user"
}],
"stream": true,
"context": {
"approach":"rrr",
Expand All @@ -52,7 +58,10 @@ POST {{api_host}}/ask
Content-Type: application/json

{
"question": "What is the refund policy?",
"messages": [{
"content": "What is the refund policy?",
"role": "user"
}],
"context": {
"approach":"rtr",
"retrieval_mode": "hybrid",
Expand All @@ -69,7 +78,10 @@ POST {{api_host}}/ask
Content-Type: application/json

{
"question": "How to contact a representative?",
"messages": [{
"content": "How to contact a representative?",
"role": "user"
}],
"context": {
"approach":"rrr",
"retrieval_mode": "hybrid",
Expand Down