Skip to content

Commit

Permalink
feat: add support for Rich Text response
Browse files Browse the repository at this point in the history
  • Loading branch information
HardeepAsrani committed Oct 7, 2024
1 parent 7e656bb commit e6ecbf2
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 23 deletions.
3 changes: 2 additions & 1 deletion inc/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ public function update_settings( $request ) {
'default_message' => function ( $value ) {
return is_string( $value );
},
'chat_model' => function ( $value ) {
'chat_model' => function ( $value ) {
return is_string( $value );
},
'temperature' => function ( $value ) {
Expand Down Expand Up @@ -723,6 +723,7 @@ function ( $row ) {
'thread_id' => $thread_id,
'query_run' => $query_run,
'record_id' => $record_id ? $record_id : null,
'content' => $article_context,
)
);
}
Expand Down
56 changes: 47 additions & 9 deletions inc/OpenAI.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class OpenAI {
*
* @var string
*/
private $prompt_version = '1.1.0';
private $prompt_version = '1.2.0';

/**
* Chat Model.
Expand Down Expand Up @@ -64,6 +64,45 @@ public function __construct( $api_key = '' ) {
}
}

/**
* Get Assistant Properties.
*
* @return array
*/
public function get_properties() {
$props = array(
'instructions' => "You are a Support Assistant tasked with providing precise, to-the-point answers based on the context provided for each query, as well as maintaining awareness of previous context for follow-up questions.\r\n\r\nCore Principles:\r\n\r\n1. Context and Question Analysis\r\n- Identify the context given in each message.\r\n- Determine the specific question to be answered based on the current context and previous interactions.\r\n\r\n2. Relevance Check\r\n- Assess if the current context or previous context contains information directly relevant to the question.\r\n- Proceed based on the following scenarios:\r\na) If current context addresses the question: Formulate a response using current context.\r\nb) If current context is empty but previous context is relevant: Use previous context to answer.\r\nc) If the input is a greeting: Respond appropriately.\r\nd) If neither current nor previous context addresses the question: Respond with an empty response and success: false.\r\n\r\n3. Response Formulation\r\n- Use information from the current context primarily. If current context is insufficient, refer to previous context for follow-up questions.\r\n- Include all relevant details, including any code snippets or links if present.\r\n- Avoid including unnecessary information.\r\n- Format the response in HTML using only these allowed tags: h2, h3, p, img, a, pre, strong, em.\r\n\r\n4. Context Reference\r\n- Do not explicitly mention or refer to the context in your answer.\r\n- Provide a straightforward response that directly answers the question.\r\n\r\n5. Response Structure\r\n- Always structure your response as a JSON object with 'response' and 'success' fields.\r\n- The 'response' field should contain the HTML-formatted answer.\r\n- The 'success' field should be a boolean indicating whether the question was successfully answered.\r\n\r\n6. Handling Follow-up Questions\r\n- Maintain awareness of previous context to answer follow-up questions.\r\n- If current context is empty but the question seems to be a follow-up, attempt to answer using previous context.\r\n\r\nExamples:\r\n\r\n1. Initial Question with Full Answer\r\nContext: The price of XYZ product is $99.99 USD.\r\nQuestion: How much does XYZ cost?\r\nResponse:\r\n{\r\n\"response\": \"<p>The price of XYZ product is $99.99 USD.</p>\",\r\n\"success\": true\r\n}\r\n\r\n2. Follow-up Question with Empty Current Context\r\nContext: [Empty]\r\nQuestion: What currency is that in?\r\nResponse:\r\n{\r\n\"response\": \"<p>The price is in USD (United States Dollars).</p>\",\r\n\"success\": true\r\n}\r\n\r\n3. No Relevant Information in Current or Previous Context\r\nContext: [Empty]\r\nQuestion: Do you offer gift wrapping?\r\nResponse:\r\n{\r\n\"response\": \"\",\r\n\"success\": false\r\n}\r\n\r\n4. Greeting\r\nQuestion: Hello!\r\nResponse:\r\n{\r\n\"response\": \"<p>Hello! How can I assist you today?</p>\",\r\n\"success\": true\r\n}\r\n\r\nError Handling:\r\nFor invalid inputs or unrecognized question formats, respond with:\r\n{\r\n\"response\": \"<p>I apologize, but I couldn't understand your question. Could you please rephrase it?</p>\",\r\n\"success\": false\r\n}\r\n\r\nHTML Usage Guidelines:\r\n- Use <h2> for main headings and <h3> for subheadings.\r\n- Wrap paragraphs in <p> tags.\r\n- Use <pre> for code snippets or formatted text.\r\n- Apply <strong> for bold and <em> for italic emphasis sparingly.\r\n- Include <img> only if specific image information is provided in the context.\r\n- Use <a> for links, ensuring they are relevant and from the provided context.\r\n\r\nRemember:\r\n- Prioritize using the current context for answers.\r\n- For follow-up questions with empty current context, refer to previous context if relevant.\r\n- If information isn't available in current or previous context, indicate this with an empty response and success: false.\r\n- Always strive to provide the most accurate and relevant information based on available context.",
'model' => $this->chat_model,
);

if ( 'gpt-4o-mini' === $this->chat_model ) {
$props['response_format'] = array(
'type' => 'json_schema',
'json_schema' => array(
'name' => 'chatbot_response',
'strict' => false,
'schema' => array(
'type' => 'object',
'properties' => array(
'response' => array(
'type' => 'string',
'description' => 'The HTML-formatted response to the user\'s question.',
),
'success' => array(
'type' => 'boolean',
'description' => 'Indicates whether the question was successfully answered from the provided context.',
),
),
'required' => array( 'success' ),
'additionalProperties' => false,
),
),
);
}

return $props;
}

/**
* Setup Assistant.
*
Expand Down Expand Up @@ -91,10 +130,11 @@ public function setup_assistant() {
public function create_assistant() {
$response = $this->request(
'assistants',
array(
'instructions' => "Assistant Role & Concise Response Guidelines: As a Support Assistant, provide precise, to-the-point answers based exclusively on the previously provided context.\r\n\r\nSET OF PRINCIPLES TO FOLLOW:\r\n\r\n1. **Identify the Context and Question**:\r\n1.1. **START CONTEXT**: Identify the context provided in the message. **: END CONTEXT**\r\n1.2. **START QUESTION**: Identify the question that needs to be answered based on the context.. **: END QUESTION**\r\n\r\n2. **Check the Context for Relevance**:\r\n2.1. Determine if the context contains information directly relevant to the question.\r\n2.2. If the context addresses the user's question, proceed to the next step.\r\n2.3. If the question is a greeting, respond appropriately with the greeting.\r\n2.4. If the context does not address the user's question, respond with: `{\"response\": \"\", \"success\": false}`.\r\n\r\n3. **Formulate the Response**:\r\n3.1. If the context is sufficient, formulate a clear and concise response using only the information provided in the context.\r\n3.2. Ensure the response includes all important details covered in the context, but avoid any extraneous information.\r\n\r\n4. **Avoid Referring to the Context**:\r\n4.1. Do not refer to the context or state that the response is based on the context in your answer.\r\n4.2. Ensure the response is straightforward and directly answers the question.\r\n\r\n5. **Generate the JSON Response**:\r\n5.1. Structure the response according to the following JSON schema:\r\n\r\n\r\n{\r\n \"\$schema\": \"http:\/\/json-schema.org\/draft-07\/schema#\",\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"response\": {\r\n \"type\": \"string\",\r\n \"description\": \"Contains the response to the question. Do not include it if the answer wasn't available in the context.\"\r\n },\r\n \"success\": {\r\n \"type\": \"boolean\",\r\n \"description\": \"Indicates whether the question was successfully answered from provided context.\"\r\n }\r\n },\r\n \"required\": [\"success\"]\r\n}\r\n\r\nExample Usage:\r\n\r\nContext: [Provide context here]\r\nQuestion: [Provide question here]\r\n\r\nExpected Behavior:\r\n\r\n- If the question is fully covered by the context, provide a response using the provided JSON schema.\r\n- If the question is not fully covered by the context, respond with: {\"response\": \"\", \"success\": false}.\r\n\r\nExample Responses:\r\n\r\n- Context covers the question: {\"response\": \"Here is the information you requested.\", \"success\": true}\r\n- Context does not cover the question: {\"response\": \"\", \"success\": false}\r\n- Context does not cover the question but is a greeting: {\"response\": \"Hello, what can I help you with?.\", \"success\": true}",
'name' => 'Chatbot by Hyve',
'model' => $this->chat_model,
array_merge(
$this->get_properties(),
array(
'name' => 'Chatbot by Hyve',
)
)
);

Expand Down Expand Up @@ -133,15 +173,13 @@ public function update_assistant() {
} else {
$response = $this->request(
'assistants/' . $this->assistant_id,
array(
'instructions' => "Assistant Role & Concise Response Guidelines: As a Support Assistant, provide precise, to-the-point answers based exclusively on the previously provided context.\r\n\r\nSET OF PRINCIPLES TO FOLLOW:\r\n\r\n1. **Identify the Context and Question**:\r\n1.1. **START CONTEXT**: Identify the context provided in the message. **: END CONTEXT**\r\n1.2. **START QUESTION**: Identify the question that needs to be answered based on the context.. **: END QUESTION**\r\n\r\n2. **Check the Context for Relevance**:\r\n2.1. Determine if the context contains information directly relevant to the question.\r\n2.2. If the context addresses the user's question, proceed to the next step.\r\n2.3. If the question is a greeting, respond appropriately with the greeting.\r\n2.4. If the context does not address the user's question, respond with: `{\"response\": \"\", \"success\": false}`.\r\n\r\n3. **Formulate the Response**:\r\n3.1. If the context is sufficient, formulate a clear and concise response using only the information provided in the context.\r\n3.2. Ensure the response includes all important details covered in the context, but avoid any extraneous information.\r\n\r\n4. **Avoid Referring to the Context**:\r\n4.1. Do not refer to the context or state that the response is based on the context in your answer.\r\n4.2. Ensure the response is straightforward and directly answers the question.\r\n\r\n5. **Generate the JSON Response**:\r\n5.1. Structure the response according to the following JSON schema:\r\n\r\n\r\n{\r\n \"\$schema\": \"http:\/\/json-schema.org\/draft-07\/schema#\",\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"response\": {\r\n \"type\": \"string\",\r\n \"description\": \"Contains the response to the question. Do not include it if the answer wasn't available in the context.\"\r\n },\r\n \"success\": {\r\n \"type\": \"boolean\",\r\n \"description\": \"Indicates whether the question was successfully answered from provided context.\"\r\n }\r\n },\r\n \"required\": [\"success\"]\r\n}\r\n\r\nExample Usage:\r\n\r\nContext: [Provide context here]\r\nQuestion: [Provide question here]\r\n\r\nExpected Behavior:\r\n\r\n- If the question is fully covered by the context, provide a response using the provided JSON schema.\r\n- If the question is not fully covered by the context, respond with: {\"response\": \"\", \"success\": false}.\r\n\r\nExample Responses:\r\n\r\n- Context covers the question: {\"response\": \"Here is the information you requested.\", \"success\": true}\r\n- Context does not cover the question: {\"response\": \"\", \"success\": false}\r\n- Context does not cover the question but is a greeting: {\"response\": \"Hello, what can I help you with?.\", \"success\": true}",
)
$this->get_properties()
);

if ( is_wp_error( $response ) ) {
return $response;
}

if ( ! isset( $response->id ) ) {
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions inc/Threads.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public static function create_thread( $title, $data ) {
array(
'time' => time(),
'sender' => $data['sender'],
'message' => $data['message'],
'message' => wp_kses_post( $data['message'] ),
),
);

Expand Down Expand Up @@ -176,7 +176,7 @@ public static function add_message( $post_id, $data ) {
$thread_data[] = array(
'time' => time(),
'sender' => $data['sender'],
'message' => $data['message'],
'message' => wp_kses_post( $data['message'] ),
);

update_post_meta( $post_id, '_hyve_thread_data', $thread_data );
Expand Down
9 changes: 6 additions & 3 deletions src/backend/parts/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,12 @@ const Messages = () => {
return (
<div
key={ index }
className="text-black max-w- max-w-[75%] min-w-[50%] flex flex-col items-start mr-auto my-3.5"
className="text-black max-w-[75%] min-w-[50%] flex flex-col items-start mr-auto my-3.5"
>
<p className="text-[13px] flex w-full break-words bg-[#ecf1fb] justify-start m-0 p-2.5 rounded-md">{ message.message }</p>
<p
className="hyve-chat-message text-[13px] flex flex-col w-full break-words bg-[#ecf1fb] justify-start m-0 p-2.5 rounded-md"
dangerouslySetInnerHTML={{ __html: message.message }}
/>
<time className="text-[10px] text-black p-1">{ date }</time>
</div>
);
Expand All @@ -177,7 +180,7 @@ const Messages = () => {
key={ index }
className="max-w-[75%] min-w-[50%] text-[white] flex flex-col items-end ml-auto my-3.5"
>
<p className="text-[13px] flex w-full break-words bg-[#1155cc] justify-end m-0 p-2.5 rounded-md">{ message.message }</p>
<p className="hyve-chat-message text-[13px] flex flex-col w-full break-words bg-[#1155cc] justify-end m-0 p-2.5 rounded-md">{ message.message }</p>
<time className="text-[10px] text-black p-1">{ date }</time>
</div>
);
Expand Down
54 changes: 54 additions & 0 deletions src/backend/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,57 @@
height: 100%;
}
}

.hyve-chat-message {
p {
font-size: 13px;
}

h1, h2, h3, h4, h5, h6 {
font-size: 13px;
margin-bottom: 12px;
}

ol, ul {
padding-left: 12px;
}

pre {
background-color: #282c34;
color: #abb2bf;
padding: 1em;
border-radius: 0;
font-family: monospace;
line-height: 1.5;
overflow-x: auto;
white-space: pre;
word-spacing: normal;
margin: 20px -10px;

* {
color: #abb2bf;
word-break: normal;
word-wrap: normal;
}

::selection {
background-color: #3e4451;
}

::before {
content: attr(data-content);
}

::before:has(+ :matches(function, const, return, let, var)) {
color: #c678dd;
}
}

code {
background: #d0effb;
border-radius: 5px;
border: none;
padding: 0 3px;
color: #333;
}
}
2 changes: 0 additions & 2 deletions src/backend/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ export const tokenize = ( post, chunk = true ) => {
content
} = post;

content = content.replace( /<[^>]+>/g, '' );

const tokens = tokenizer.encode( content );

const article = {
Expand Down
21 changes: 19 additions & 2 deletions src/frontend/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ class App {
add( message, sender, id = null ) {
const time = new Date();

message = this.sanitize( message );
if ( 'user' === sender ) {
message = this.sanitize( message );
}

message = this.addTargetBlack( message );

this.messages.push({ time, message, sender, id });
this.addMessage( time, message, sender, id );
Expand All @@ -89,6 +93,19 @@ class App {
return tempDiv.innerHTML;
}

addTargetBlack( message ) {
const tempDiv = document.createElement( 'div' );
tempDiv.innerHTML = message;

const links = tempDiv.querySelectorAll( 'a' );

links.forEach( link => {
link.target = '_blank';
});

return tempDiv.innerHTML;
}

setThreadID( threadID ) {
this.threadID = threadID;
}
Expand Down Expand Up @@ -204,7 +221,7 @@ class App {

const date = `${ String( datetime.getDate() ).padStart( 2, 0 ) }/${ String( datetime.getMonth() + 1 ).padStart( 2, 0 ) }/${ datetime.getFullYear() } ${ String( datetime.getHours() ).padStart( 2, 0 ) }:${ String( datetime.getMinutes() ).padStart( 2, 0 ) } ${ 12 <= datetime.getHours() ? 'PM' : 'AM' }`;

let messageHTML = `<p>${message}</p>`;
let messageHTML = `<div>${message}</div>`;

if ( null === id ) {
messageHTML += `<time datetime="${time}">${date}</time>`;
Expand Down
64 changes: 60 additions & 4 deletions src/frontend/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -239,16 +239,28 @@
display: flex;
flex-direction: column;

p {
div {
font-size: 13px;
display: flex;
padding: 10px;
border-radius: 5px;
width: 100%;
overflow-wrap: break-word;
margin: 0;
}

p {
font-size: 13px;
}

h1, h2, h3, h4, h5, h6 {
font-size: 13px;
margin-bottom: 12px;
}

ol, ul {
padding-left: 12px;
}

time {
font-size: 10px;
padding: 4px;
Expand All @@ -274,7 +286,7 @@
color: #000000;
}

p {
div {
background-color: var( --user_background, #1155cc );
justify-content: flex-end;
}
Expand All @@ -295,9 +307,53 @@
color: #ffffff;
}

p {
div {
background-color: var( --assistant_background, #ecf1fb );
justify-content: flex-start;

p {
margin: 0px;
padding: 0px;
}

pre {
background-color: #282c34;
color: #abb2bf;
padding: 1em;
border-radius: 0;
font-family: monospace;
line-height: 1.5;
overflow-x: auto;
white-space: pre;
word-spacing: normal;
margin: 20px -10px;

* {
color: #abb2bf;
word-break: normal;
word-wrap: normal;
}

::selection {
background-color: #3e4451;
}

::before {
content: attr(data-content);
}

::before:has(+ :matches(function, const, return, let, var)) {
color: #c678dd;
}
}

code {
background: #d0effb;
border-radius: 5px;
border: none;
padding: 0 3px;
color: #333;
}
}
}

Expand Down

0 comments on commit e6ecbf2

Please sign in to comment.