(core) Send hash of user ID in OpenAI API requests

Summary: Following recommendation in https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids

Test Plan: Checked that running server test shows log of hash of 'user id' (which is null because it's a fake session)

Reviewers: dsagal

Reviewed By: dsagal

Subscribers: paulfitz, georgegevoian

Differential Revision: https://phab.getgrist.com/D3958
This commit is contained in:
Alex Hall 2023-07-20 19:34:25 +02:00
parent 0a34292536
commit 5a703a1972

View File

@ -6,9 +6,11 @@ import {AssistanceMessage, AssistanceRequest, AssistanceResponse} from 'app/comm
import {delay} from 'app/common/delay'; import {delay} from 'app/common/delay';
import {DocAction} from 'app/common/DocActions'; import {DocAction} from 'app/common/DocActions';
import {ActiveDoc} from 'app/server/lib/ActiveDoc'; import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {OptDocSession} from 'app/server/lib/DocSession'; import {getDocSessionUser, OptDocSession} from 'app/server/lib/DocSession';
import log from 'app/server/lib/log'; import log from 'app/server/lib/log';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import {createHash} from "crypto";
import {getLogMetaFromDocSession} from "./serverUtils";
// These are mocked/replaced in tests. // These are mocked/replaced in tests.
// fetch is also replacing in the runCompletion script to add caching. // fetch is also replacing in the runCompletion script to add caching.
@ -179,7 +181,8 @@ export class OpenAIAssistant implements Assistant {
}); });
} }
const completion: string = await this._getCompletion(messages); const userIdHash = getUserHash(optSession);
const completion: string = await this._getCompletion(messages, userIdHash);
const response = await completionToResponse(doc, request, completion, completion); const response = await completionToResponse(doc, request, completion, completion);
if (chatMode) { if (chatMode) {
response.state = {messages}; response.state = {messages};
@ -198,7 +201,7 @@ export class OpenAIAssistant implements Assistant {
return response; return response;
} }
private async _fetchCompletion(messages: AssistanceMessage[], longerContext: boolean) { private async _fetchCompletion(messages: AssistanceMessage[], userIdHash: string, longerContext: boolean) {
const apiResponse = await DEPS.fetch( const apiResponse = await DEPS.fetch(
this._endpoint, this._endpoint,
{ {
@ -214,6 +217,7 @@ export class OpenAIAssistant implements Assistant {
temperature: 0, temperature: 0,
model: longerContext ? OpenAIAssistant.LONGER_CONTEXT_MODEL : OpenAIAssistant.DEFAULT_MODEL, model: longerContext ? OpenAIAssistant.LONGER_CONTEXT_MODEL : OpenAIAssistant.DEFAULT_MODEL,
stop: this._chatMode ? undefined : ["\n\n"], stop: this._chatMode ? undefined : ["\n\n"],
user: userIdHash,
}), }),
}, },
); );
@ -240,14 +244,16 @@ export class OpenAIAssistant implements Assistant {
return result; return result;
} }
private async _fetchCompletionWithRetries(messages: AssistanceMessage[], longerContext: boolean): Promise<any> { private async _fetchCompletionWithRetries(
messages: AssistanceMessage[], userIdHash: string, longerContext: boolean
): Promise<any> {
const maxAttempts = 3; const maxAttempts = 3;
for (let attempt = 1; ; attempt++) { for (let attempt = 1; ; attempt++) {
try { try {
return await this._fetchCompletion(messages, longerContext); return await this._fetchCompletion(messages, userIdHash, longerContext);
} catch (e) { } catch (e) {
if (e instanceof SwitchToLongerContext) { if (e instanceof SwitchToLongerContext) {
return await this._fetchCompletionWithRetries(messages, true); return await this._fetchCompletionWithRetries(messages, userIdHash, true);
} else if (e instanceof NonRetryableError) { } else if (e instanceof NonRetryableError) {
throw e; throw e;
} else if (attempt === maxAttempts) { } else if (attempt === maxAttempts) {
@ -259,8 +265,8 @@ export class OpenAIAssistant implements Assistant {
} }
} }
private async _getCompletion(messages: AssistanceMessage[]) { private async _getCompletion(messages: AssistanceMessage[], userIdHash: string) {
const result = await this._fetchCompletionWithRetries(messages, false); const result = await this._fetchCompletionWithRetries(messages, userIdHash, false);
const completion: string = String(this._chatMode ? result.choices[0].message.content : result.choices[0].text); const completion: string = String(this._chatMode ? result.choices[0].message.content : result.choices[0].text);
if (this._chatMode) { if (this._chatMode) {
messages.push(result.choices[0].message); messages.push(result.choices[0].message);
@ -433,3 +439,15 @@ async function completionToResponse(doc: AssistanceDoc, request: AssistanceReque
reply, reply,
}; };
} }
function getUserHash(session: OptDocSession): string {
const user = getDocSessionUser(session);
// Make it a bit harder to guess the user ID.
const salt = "7a8sb6987asdb678asd687sad6boas7f8b6aso7fd";
const hashSource = `${user?.id} ${user?.ref} ${salt}`;
const hash = createHash('sha256').update(hashSource).digest('base64');
// So that if we get feedback about a user ID hash, we can
// search for the hash in the logs to find the original user ID.
log.rawInfo("getUserHash", {...getLogMetaFromDocSession(session), userRef: user?.ref, hash});
return hash;
}