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

Adding System Messages #13

Open
tshirtblue opened this issue Jun 18, 2023 · 0 comments
Open

Adding System Messages #13

tshirtblue opened this issue Jun 18, 2023 · 0 comments

Comments

@tshirtblue
Copy link

I was attempting to add system messages to the repository to allow for prompt engineering. However, attempts are turning up flat. Any advice? Likely linked to lack of knowledge of codebase but working through it I'm not finding where else I'd need to make changes. Find below proposed changes to src/api/index.ts file

import { ChatGptError, ChatGptResponse, SendMessageParams } from '../types';
import {
  CHAT_PAGE,
  HOST_URL,
  LOGIN_PAGE,
  PROMPT_ENDPOINT,
  REQUEST_DEFAULT_TIMEOUT,
  STREAMED_REQUEST_DEFAULT_TIMEOUT,
} from '../constants';
import parseStreamedGptResponse from '../utils/parseStreamedGptResponse';
import getChatGptConversationHeaders from '../utils/getChatGptConversationHeaders';
import type { RefObject } from 'react';
import type WebView from 'react-native-webview';
import wait from '../utils/wait';
import { getStatusText } from '../utils/httpCodes';

let webview: RefObject<WebView>['current'];

export const init = (webviewRef: RefObject<WebView>['current']) => {
  webview = webviewRef;
};

/**
 * Monkey patches fetch to intercept ChatGPT requests and read the JWT
 * It also injects 2 methods in the global scope to accomplish the following:
 * 1. Sending messages to the ChatGPT backend directly from the Webview and stream the response back to RN
 * 2. Removing the theme switcher button from the webview when GPT shows it's at full capacity
 *
 * Note: It'd be cool to define the function in normal JS and
 * use fn.toString() or`${fn}` and wrap it in a IIFE,
 * but babel messes up the transformations of async/await and breaks the injected code.
 */
const systemPrompt = 'You are a baby name generator';

export const createGlobalFunctionsInWebviewContext = () => {
  return `
    const { fetch: originalFetch } = window;
    window.fetch = async (...args) => {
      const [resource, config] = args;
      window.ReactNativeWebView.postMessage(JSON.stringify({type: 'REQUEST_INTERCEPTED_CONFIG', payload: config}));
      const response = await originalFetch(resource, config);
      return response;
    };

    window.removeThemeSwitcher = () => {
      const svgIcon = document.querySelector("button > svg");
      if (!svgIcon) {
        return;
      }
      const themeSwitchButton = svgIcon.closest('button');
      if (themeSwitchButton) {
        themeSwitchButton.style.display = 'none';
      }
    };
    
  window.sendGptMessage = async ({
      accessToken,
      message,
      messageId,
      newMessageId,
      newSystemMessageId,
      conversationId,
      prompt,
      systemPrompt,
      timeout
    }) => {

      async function* streamAsyncIterable(stream) {
        const reader = stream.getReader()
        try {
          while (true) {
            const { done, value } = await reader.read()
            if (done) {
              return
            }
            yield value
          }
        } finally {
          reader.releaseLock()
        }
      }

      function getHeaders(accessToken) {
        return {
          accept: "text/event-stream",
          "x-openai-assistant-app-id": "",
          authorization: accessToken,
          "content-type": "application/json",
          origin: "${HOST_URL}",
          referrer: "${CHAT_PAGE}",
          "sec-fetch-mode": "cors",
          "sec-fetch-site": "same-origin",
          "x-requested-with": "com.chatgpt3auth"
        };
      }

      const url = "${PROMPT_ENDPOINT}";
      const body = {
        action: "next",
        messages: [
        {
            role: "system",
            content: {
              content_type: "text",
              parts: [systemPrompt],
            },
         },
         {
            id: newMessageId,
            role: "user",
            content: {
              content_type: "text",
              parts: [message],
            },
          },
        ],
        model: "gpt-3.5-turbo-0613",
        parent_message_id: messageId,
      };

      if (conversationId) {
        body.conversation_id = conversationId;
      }

      const headers = getHeaders(accessToken);

      try {

        const controller = new AbortController();

        const timeoutId = setTimeout(() => {
          // Notifying RN that the request timed out
          window.ReactNativeWebView.postMessage(JSON.stringify({type: 'STREAM_ERROR', payload: {status: 408, statusText: 'Request timed out'}}));
          controller.abort();
        }, timeout);

        const res = await fetch(url, {
          method: "POST",
          body: JSON.stringify(body),
          headers: headers,
          mode: "cors",
          credentials: "include",
          signal: controller.signal
        });

        clearTimeout(timeoutId);

        if (res.status >= 400 && res.status < 600) {
          window.ReactNativeWebView.postMessage(JSON.stringify({type: 'STREAM_ERROR', payload: {status: res.status}}));
          return true;
        }

        for await (const chunk of streamAsyncIterable(res.body)) {
          const str = new TextDecoder().decode(chunk);
          window.ReactNativeWebView.postMessage(JSON.stringify({type: 'RAW_ACCUMULATED_RESPONSE', payload: str}));
        }
      } catch (e) {
        // Nothing to do here
      }
    };

    true;
  `;
};

/**
 * Calls a global function in the Webview window object to send a streamed message
 */
export function postStreamedMessage({
  accessToken,
  message,
  messageId = uuid.v4() as string,
  conversationId,
  timeout = STREAMED_REQUEST_DEFAULT_TIMEOUT,
}: SendMessageParams) {
  const newMessageId = uuid.v4() as string;
  const newSystemMessageId = uuid.v4() as string;
  let script = '';
  if (conversationId) {
    script = `
      window.sendGptMessage({
        accessToken: "${accessToken}",
        message: "${message}",
        messageId: "${messageId}",
        newMessageId: "${newMessageId}",
        conversationId: "${conversationId}",
        systemPrompt: "${systemPrompt.replace(/"/g, '\\"')}",
        timeout: ${timeout}
      });

      true;
    `;
  } else {
    script = `
      window.sendGptMessage({
        accessToken: "${accessToken}",
        message: "${message}",
        messageId: "${messageId}",
        newMessageId: "${newMessageId}",
        systemPrompt: "${systemPrompt}",        
        timeout: ${timeout}
      });

      true;
    `;
  }

  webview?.injectJavaScript(script);
}

/**
 * Sends a normal message to the ChatGPT conversation backend endpoint
 */
export async function postMessage({
  accessToken,
  message,
  messageId = uuid.v4() as string,
  conversationId,
  systemPrompt,
  timeout = REQUEST_DEFAULT_TIMEOUT,
  onTokenExpired,
}: SendMessageParams): Promise<ChatGptResponse> {
  const controller = new AbortController();
  const newMessageId = uuid.v4() as string;
  const newSystemMessageId = uuid.v4() as string;
  
  const timeoutId = setTimeout(() => {
    controller.abort();
    const error = new ChatGptError(
      'ChatGPTResponseClientError: Request timed out'
    );
    error.statusCode = 408;
    throw error;
  }, timeout);

  const url = PROMPT_ENDPOINT;
  const body = {
    action: 'next',
    messages: [
     {
         role: "system",
         content: {
           content_type: "text",
           parts: [systemPrompt],
       },
     },
     {
        id: newMessageId,
        role: 'user',
        content: {
          content_type: 'text',
          parts: [message],
        },
      },
    ],
    model: 'gpt-3.5-turbo-0613',
    parent_message_id: messageId,
  };

  if (conversationId) {
    // @ts-ignore
    body.conversation_id = conversationId;
  }

  const res = await fetch(url, {
    method: 'POST',
    body: JSON.stringify(body),
    headers: getChatGptConversationHeaders(accessToken),
    mode: 'cors',
    signal: controller.signal,
  });

  clearTimeout(timeoutId);

  if (res.status >= 400 && res.status < 500) {
    if (res.status === 401) {
      // Token expired, notifying
      onTokenExpired?.();
    } else if (res.status === 403) {
      // Session expired, reloading Web View
      reloadWebView();
    }

    const error = new ChatGptError(getStatusText(res.status as any));
    error.statusCode = res.status;
    throw error;
  } else if (res.status >= 500) {
    const error = new ChatGptError(
      `ChatGPTResponseServerError: ${res.status} ${res.statusText}`
    );
    error.statusCode = res.status;
    throw error;
  }

  const rawText = await res.text();
  const parsedData = parseStreamedGptResponse(rawText);

  if (!parsedData) {
    throw new ChatGptError('ChatGPTResponseError: Unable to parse response');
  }

  return parsedData;
}

export function reloadWebView() {
  webview?.reload();
}

/**
 * Removes the icon button in the top right corner of the webview screen when
 * ChatGPT is at full capacity
 */
export async function removeThemeSwitcher() {
  // Apparently the button is not there yet after the page loads, so we wait a bit
  await wait(200);

  const script = `
    (() => {
      const xpath = "//div[contains(text(),'ChatGPT is at capacity right now')]";
      const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
      if (element) {
        window.removeThemeSwitcher();
      }
    })();

    true;
  `;

  webview?.injectJavaScript(script);
}

/**
 * Checks if ChatGPT servers are overloaded and the normal login page is not accessible
 */
export function checkFullCapacity() {
  const script = `
    (() => {
      const xpath = "//div[contains(text(),'ChatGPT is at capacity right now')]";
      const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
      if (element) {
        window.removeThemeSwitcher();
        window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'CHAT_GPT_FULL_CAPACITY' }));
      }
    })();

    true;
  `;
  webview?.injectJavaScript(script);
}

/**
 * Refreshes the webview and checks again if the login page is available
 */
export async function retryLogin() {
  reloadWebView();
  // Waiting 3 seconds before checking again
  await wait(3000);
  checkFullCapacity();
}

export function navigateToLoginPage() {
  const script = `
   (() => {
      window.location.replace("${LOGIN_PAGE}");
   })();

   true;
  `;

  webview?.injectJavaScript(script);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant