@cobrowser/chatgpt 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/models/ChatGptMessage.d.ts +3 -0
- package/dist/models/ChatGptResponse.d.ts +6 -0
- package/dist/models/ChatGptResponse.js +2 -0
- package/dist/models/ChatGptUsageTokens.d.ts +6 -0
- package/dist/models/ChatGptUsageTokens.js +2 -0
- package/dist/services/BaseService/BaseService.d.ts +11 -2
- package/dist/services/BaseService/BaseService.js +24 -2
- package/dist/services/ChatService/ChatService.d.ts +1 -1
- package/dist/services/ChatService/ChatService.js +2 -2
- package/dist/services/CopilotService/CopilotService.d.ts +10 -2
- package/dist/services/CopilotService/CopilotService.js +65 -14
- package/dist/services/TranslationService/TranslationService.d.ts +7 -2
- package/dist/services/TranslationService/TranslationService.js +73 -5
- package/package.json +4 -3
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
|
2
|
+
import ChatGptUsageTokens from '../../models/ChatGptUsageTokens';
|
|
2
3
|
declare class BaseService {
|
|
3
4
|
request: AxiosInstance;
|
|
4
5
|
readonly BASE_CHAT_COMPLETION_URL = "https://api.openai.com/v1/chat/completions";
|
|
6
|
+
openaiApiKey: string | undefined;
|
|
5
7
|
chatGptModel: string;
|
|
6
8
|
maxNumberOfChatCompletionResult: number;
|
|
7
9
|
/**
|
|
8
10
|
*
|
|
9
|
-
* @param
|
|
11
|
+
* @param apiKey
|
|
10
12
|
* @param model the main chatGPT model to use
|
|
13
|
+
* @param apiUrl Api url to be used to make API calls to openAI services
|
|
11
14
|
* @param maxNumberOfChoices How many chat completion choices to generate for each request.
|
|
15
|
+
*
|
|
12
16
|
*/
|
|
13
|
-
constructor(
|
|
17
|
+
constructor(apiKey?: string | undefined, model?: string, apiUrl?: string, maxNumberOfChoices?: number);
|
|
14
18
|
/**
|
|
15
19
|
* Filter the response from chatGpt API and only get the first choice
|
|
16
20
|
* @param response
|
|
17
21
|
*/
|
|
18
22
|
getResponseFirstChoice(response: AxiosResponse): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Filter the response from chatGpt API and get the usage tokens
|
|
25
|
+
* @param response
|
|
26
|
+
*/
|
|
27
|
+
getResponseUsageTokens(response: AxiosResponse): ChatGptUsageTokens | undefined;
|
|
19
28
|
/**
|
|
20
29
|
* function to handle requests errors
|
|
21
30
|
* @param error
|
|
@@ -8,11 +8,13 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
8
8
|
class BaseService {
|
|
9
9
|
/**
|
|
10
10
|
*
|
|
11
|
-
* @param
|
|
11
|
+
* @param apiKey
|
|
12
12
|
* @param model the main chatGPT model to use
|
|
13
|
+
* @param apiUrl Api url to be used to make API calls to openAI services
|
|
13
14
|
* @param maxNumberOfChoices How many chat completion choices to generate for each request.
|
|
15
|
+
*
|
|
14
16
|
*/
|
|
15
|
-
constructor(
|
|
17
|
+
constructor(apiKey = process.env.OPEN_AI_API_KEY, model = 'gpt-3.5-turbo', apiUrl, maxNumberOfChoices = 1) {
|
|
16
18
|
Object.defineProperty(this, "request", {
|
|
17
19
|
enumerable: true,
|
|
18
20
|
configurable: true,
|
|
@@ -25,6 +27,12 @@ class BaseService {
|
|
|
25
27
|
writable: true,
|
|
26
28
|
value: 'https://api.openai.com/v1/chat/completions'
|
|
27
29
|
});
|
|
30
|
+
Object.defineProperty(this, "openaiApiKey", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: void 0
|
|
35
|
+
});
|
|
28
36
|
Object.defineProperty(this, "chatGptModel", {
|
|
29
37
|
enumerable: true,
|
|
30
38
|
configurable: true,
|
|
@@ -44,6 +52,7 @@ class BaseService {
|
|
|
44
52
|
Authorization: `Bearer ${apiKey} `,
|
|
45
53
|
},
|
|
46
54
|
});
|
|
55
|
+
this.openaiApiKey = apiKey;
|
|
47
56
|
this.chatGptModel = model;
|
|
48
57
|
this.maxNumberOfChatCompletionResult = maxNumberOfChoices;
|
|
49
58
|
}
|
|
@@ -60,6 +69,19 @@ class BaseService {
|
|
|
60
69
|
}
|
|
61
70
|
return undefined;
|
|
62
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Filter the response from chatGpt API and get the usage tokens
|
|
74
|
+
* @param response
|
|
75
|
+
*/
|
|
76
|
+
getResponseUsageTokens(response) {
|
|
77
|
+
var _a;
|
|
78
|
+
if ((_a = response.data) === null || _a === void 0 ? void 0 : _a.usage) {
|
|
79
|
+
const usage = response.data.usage;
|
|
80
|
+
logger_1.default.info(`ChatGpt response usage tokens: ${JSON.stringify(usage)}`);
|
|
81
|
+
return usage;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
63
85
|
/**
|
|
64
86
|
* function to handle requests errors
|
|
65
87
|
* @param error
|
|
@@ -3,6 +3,6 @@ import ConversationHistory from 'src/models/ConversationHistory';
|
|
|
3
3
|
export declare class ChatService extends BaseService {
|
|
4
4
|
#private;
|
|
5
5
|
model: string | undefined;
|
|
6
|
-
constructor(
|
|
6
|
+
constructor(apiKey?: string, model?: string, apiUrl?: string, maxNumberOfChoices?: number);
|
|
7
7
|
ask(context: string | undefined, conversationHistory: ConversationHistory[]): Promise<string | undefined>;
|
|
8
8
|
}
|
|
@@ -24,8 +24,8 @@ const BaseService_1 = __importDefault(require("../BaseService/BaseService"));
|
|
|
24
24
|
const logger_1 = __importDefault(require("../logger"));
|
|
25
25
|
const constants_1 = require("../../constants");
|
|
26
26
|
class ChatService extends BaseService_1.default {
|
|
27
|
-
constructor(
|
|
28
|
-
super(
|
|
27
|
+
constructor(apiKey, model, apiUrl, maxNumberOfChoices) {
|
|
28
|
+
super(apiKey, model, apiUrl, maxNumberOfChoices);
|
|
29
29
|
_ChatService_instances.add(this);
|
|
30
30
|
Object.defineProperty(this, "model", {
|
|
31
31
|
enumerable: true,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import ChatGptResponse from '../../models/ChatGptResponse';
|
|
1
2
|
import BaseService from '../BaseService/BaseService';
|
|
2
3
|
import ChatGptMessage from '../../models/ChatGptMessage';
|
|
3
4
|
import Item from '../../models/Item';
|
|
4
5
|
export declare class CopilotService extends BaseService {
|
|
5
|
-
|
|
6
|
+
assistantId: string | undefined;
|
|
7
|
+
constructor(apiKey?: string, model?: string, assistantId?: string, apiUrl?: string, maxNumberOfChoices?: number);
|
|
6
8
|
/**
|
|
7
9
|
* a method to be used to suggest some quick replies for the agent based on the conversation
|
|
8
10
|
* will be added as a single string in the format
|
|
@@ -15,11 +17,17 @@ export declare class CopilotService extends BaseService {
|
|
|
15
17
|
* this will make it easy to ask chatGpt for quick replies
|
|
16
18
|
* @param conversation
|
|
17
19
|
*/
|
|
18
|
-
suggest(conversation: string): Promise<
|
|
20
|
+
suggest(conversation: string): Promise<ChatGptResponse | undefined>;
|
|
19
21
|
/**
|
|
20
22
|
* this will request chatGPT to recommend product or item based on the whole conversation
|
|
21
23
|
* @param conversation the whole chat conversation
|
|
22
24
|
* @param items the items to recommend from
|
|
23
25
|
*/
|
|
24
26
|
recommend(conversation: ChatGptMessage[], items: Item[]): Promise<string | undefined>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the reply of the assistant if the assistant_id is available
|
|
29
|
+
*
|
|
30
|
+
* @param message
|
|
31
|
+
*/
|
|
32
|
+
getAssistantReply(message: string): Promise<string | undefined>;
|
|
25
33
|
}
|
|
@@ -16,9 +16,17 @@ exports.CopilotService = void 0;
|
|
|
16
16
|
const BaseService_1 = __importDefault(require("../BaseService/BaseService"));
|
|
17
17
|
const logger_1 = __importDefault(require("../logger"));
|
|
18
18
|
const ChatGptMessage_1 = require("../../models/ChatGptMessage");
|
|
19
|
+
const openai_1 = __importDefault(require("openai"));
|
|
19
20
|
class CopilotService extends BaseService_1.default {
|
|
20
|
-
constructor(
|
|
21
|
-
super(
|
|
21
|
+
constructor(apiKey, model, assistantId, apiUrl, maxNumberOfChoices) {
|
|
22
|
+
super(apiKey, model, apiUrl, maxNumberOfChoices);
|
|
23
|
+
Object.defineProperty(this, "assistantId", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: void 0
|
|
28
|
+
});
|
|
29
|
+
this.assistantId = assistantId;
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
24
32
|
* a method to be used to suggest some quick replies for the agent based on the conversation
|
|
@@ -39,21 +47,37 @@ class CopilotService extends BaseService_1.default {
|
|
|
39
47
|
return Promise.reject(new Error('conversation must be provided'));
|
|
40
48
|
}
|
|
41
49
|
try {
|
|
50
|
+
if (this.assistantId) {
|
|
51
|
+
// Here we get the last customer message as we dont need the whole conversation
|
|
52
|
+
// as we are doing with the other approach
|
|
53
|
+
const conversationArray = conversation.split('\n');
|
|
54
|
+
const lastCustomerMessage = conversationArray.reverse().find(e => e.includes('customer:'));
|
|
55
|
+
if (!lastCustomerMessage) {
|
|
56
|
+
logger_1.default.error('last customer message not found');
|
|
57
|
+
return Promise.reject(new Error('last customer message not found'));
|
|
58
|
+
}
|
|
59
|
+
const assistantReply = yield this.getAssistantReply(lastCustomerMessage);
|
|
60
|
+
return {
|
|
61
|
+
firstChoice: assistantReply
|
|
62
|
+
};
|
|
63
|
+
}
|
|
42
64
|
// main prompt used to instruct chatGPT to suggest some quick replies
|
|
43
65
|
const mainPrompt = `Propose some quick replies for the agent to be used on the following conversation
|
|
44
|
-
display your response as unordered list:-\n${conversation}
|
|
66
|
+
display your response as unordered list:-\n${conversation}`,
|
|
45
67
|
// main prompt will be the whole conversation as string
|
|
46
|
-
|
|
68
|
+
mainPromptMessage = {
|
|
47
69
|
role: ChatGptMessage_1.ChatGptRole.USER,
|
|
48
70
|
content: mainPrompt,
|
|
49
|
-
}
|
|
71
|
+
},
|
|
50
72
|
// the body we need to send to the request
|
|
51
|
-
|
|
73
|
+
requestBody = {
|
|
52
74
|
model: this.chatGptModel,
|
|
53
75
|
messages: [mainPromptMessage],
|
|
76
|
+
}, response = yield this.request.post('', requestBody);
|
|
77
|
+
return {
|
|
78
|
+
firstChoice: this.getResponseFirstChoice(response),
|
|
79
|
+
usageTokens: this.getResponseUsageTokens(response),
|
|
54
80
|
};
|
|
55
|
-
const response = yield this.request.post('', requestBody);
|
|
56
|
-
return this.getResponseFirstChoice(response);
|
|
57
81
|
}
|
|
58
82
|
catch (e) {
|
|
59
83
|
this.handleErrors(e);
|
|
@@ -74,18 +98,17 @@ class CopilotService extends BaseService_1.default {
|
|
|
74
98
|
}
|
|
75
99
|
try {
|
|
76
100
|
// main prompt used to instruct chatGPT to recommend products
|
|
77
|
-
const mainPrompt = `we have this list of Items: ${JSON.stringify(items)}, what to recommend for you
|
|
101
|
+
const mainPrompt = `we have this list of Items: ${JSON.stringify(items)}, what to recommend for you`,
|
|
78
102
|
// main prompt will be the first message of the whole conversation
|
|
79
|
-
|
|
103
|
+
firstMessage = {
|
|
80
104
|
role: ChatGptMessage_1.ChatGptRole.ASSISTANT,
|
|
81
105
|
content: mainPrompt,
|
|
82
|
-
}
|
|
106
|
+
},
|
|
83
107
|
// the body we need to send to the request
|
|
84
|
-
|
|
108
|
+
requestBody = {
|
|
85
109
|
model: this.chatGptModel,
|
|
86
110
|
messages: [firstMessage, ...conversation],
|
|
87
|
-
};
|
|
88
|
-
const response = yield this.request.post('', requestBody);
|
|
111
|
+
}, response = yield this.request.post('', requestBody);
|
|
89
112
|
return this.getResponseFirstChoice(response);
|
|
90
113
|
}
|
|
91
114
|
catch (e) {
|
|
@@ -94,5 +117,33 @@ class CopilotService extends BaseService_1.default {
|
|
|
94
117
|
}
|
|
95
118
|
});
|
|
96
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Get the reply of the assistant if the assistant_id is available
|
|
122
|
+
*
|
|
123
|
+
* @param message
|
|
124
|
+
*/
|
|
125
|
+
getAssistantReply(message) {
|
|
126
|
+
var _a;
|
|
127
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
128
|
+
const openai = new openai_1.default({ apiKey: this.openaiApiKey });
|
|
129
|
+
let reply = undefined;
|
|
130
|
+
const thread = yield openai.beta.threads.create();
|
|
131
|
+
yield openai.beta.threads.messages.create(thread.id, { role: "user", content: message });
|
|
132
|
+
let run = yield openai.beta.threads.runs.create(thread.id, { assistant_id: (_a = this.assistantId) !== null && _a !== void 0 ? _a : '' });
|
|
133
|
+
while (['queued', 'in_progress', 'cancelling'].includes(run.status)) {
|
|
134
|
+
yield new Promise(resolve => setTimeout(resolve, 500));
|
|
135
|
+
run = yield openai.beta.threads.runs.retrieve(run.thread_id, run.id);
|
|
136
|
+
}
|
|
137
|
+
if (run.status === 'completed') {
|
|
138
|
+
const { data } = yield openai.beta.threads.messages.list(run.thread_id);
|
|
139
|
+
const messages = data.flatMap((message) => message === null || message === void 0 ? void 0 : message.content.map((content) => ((content.type === 'text' && message.role === 'assistant') ? content.text : '')));
|
|
140
|
+
const assistantMessage = messages.find(message => typeof message === 'object' && 'value' in message && typeof message.value === 'string');
|
|
141
|
+
if (assistantMessage) {
|
|
142
|
+
reply = assistantMessage.value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return reply;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
97
148
|
}
|
|
98
149
|
exports.CopilotService = CopilotService;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import ChatGptResponse from '../../models/ChatGptResponse';
|
|
1
2
|
import BaseService from '../BaseService/BaseService';
|
|
2
3
|
export declare class TranslationService extends BaseService {
|
|
3
|
-
|
|
4
|
+
SYSTEM_ROLE_MESSAGE: string;
|
|
5
|
+
FIND_TEXT_LANGUAGE_SYSTEM_ROLE: string;
|
|
6
|
+
constructor(apiKey?: string, model?: string, apiUrl?: string, maxNumberOfChoices?: number);
|
|
4
7
|
/**
|
|
5
8
|
* request the chatGpt Api to translate a specific test to a desired language
|
|
6
9
|
* @param text the text to translate
|
|
7
10
|
* @param toLanguage the desired language to translate to ( English is the default )
|
|
8
11
|
* @param fromLanguage if we want to specify the language of the text, we can use this ( optional)
|
|
9
12
|
*/
|
|
10
|
-
translate(text: string, toLanguage?: string, fromLanguage?: string): Promise<
|
|
13
|
+
translate(text: string, toLanguage?: string, fromLanguage?: string): Promise<ChatGptResponse | undefined>;
|
|
14
|
+
findTextLanguage(text: string): Promise<ChatGptResponse | undefined>;
|
|
15
|
+
getFindTextLanguagePromptMessage(text: string): string;
|
|
11
16
|
}
|
|
@@ -18,8 +18,26 @@ const BaseService_1 = __importDefault(require("../BaseService/BaseService"));
|
|
|
18
18
|
const logger_1 = __importDefault(require("../logger"));
|
|
19
19
|
const constants_1 = require("../../constants");
|
|
20
20
|
class TranslationService extends BaseService_1.default {
|
|
21
|
-
constructor(
|
|
22
|
-
super(
|
|
21
|
+
constructor(apiKey, model, apiUrl, maxNumberOfChoices) {
|
|
22
|
+
super(apiKey, model, apiUrl, maxNumberOfChoices);
|
|
23
|
+
Object.defineProperty(this, "SYSTEM_ROLE_MESSAGE", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: 'You are a translator that provides a response in JSON format only. '
|
|
28
|
+
+ 'The response should be a JSON object with a boolean called \'error\' and \'data,\''
|
|
29
|
+
+ ' which is an array of objects. Each object in the array will contain a \'translation\' property, '
|
|
30
|
+
+ 'holding the translation of the message, and a \'translated_message\' property, '
|
|
31
|
+
+ 'which will contain the original message before being translated.'
|
|
32
|
+
+ ' Additionally, the \'translation\' property will hold the translation of the \'translated_message\' property.'
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "FIND_TEXT_LANGUAGE_SYSTEM_ROLE", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: 'You are a translator that provides a response in JSON format only—a JSON that contains'
|
|
39
|
+
+ ' \'languageCode\' and \'isError\' if you cannot detect the language.'
|
|
40
|
+
});
|
|
23
41
|
}
|
|
24
42
|
/**
|
|
25
43
|
* request the chatGpt Api to translate a specific test to a desired language
|
|
@@ -39,14 +57,24 @@ class TranslationService extends BaseService_1.default {
|
|
|
39
57
|
// the body we need to send to the request
|
|
40
58
|
requestBody = {
|
|
41
59
|
model: this.chatGptModel,
|
|
42
|
-
|
|
60
|
+
response_format: { type: 'json_object' },
|
|
61
|
+
messages: [
|
|
62
|
+
{
|
|
63
|
+
role: ChatGptMessage_1.ChatGptRole.SYSTEM,
|
|
64
|
+
content: this.SYSTEM_ROLE_MESSAGE,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
43
67
|
role: ChatGptMessage_1.ChatGptRole.USER,
|
|
44
68
|
content: translateText,
|
|
45
|
-
}
|
|
69
|
+
},
|
|
70
|
+
],
|
|
46
71
|
};
|
|
47
72
|
try {
|
|
48
73
|
const response = yield this.request.post('', requestBody);
|
|
49
|
-
return
|
|
74
|
+
return {
|
|
75
|
+
firstChoice: this.getResponseFirstChoice(response),
|
|
76
|
+
usageTokens: this.getResponseUsageTokens(response),
|
|
77
|
+
};
|
|
50
78
|
}
|
|
51
79
|
catch (e) {
|
|
52
80
|
this.handleErrors(e);
|
|
@@ -54,5 +82,45 @@ class TranslationService extends BaseService_1.default {
|
|
|
54
82
|
}
|
|
55
83
|
});
|
|
56
84
|
}
|
|
85
|
+
findTextLanguage(text) {
|
|
86
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
if (!text) {
|
|
88
|
+
logger_1.default.error('translation text should be provided');
|
|
89
|
+
return Promise.reject(new Error('text must be provided'));
|
|
90
|
+
}
|
|
91
|
+
// instruction on what to do with the text
|
|
92
|
+
const translateText = this.getFindTextLanguagePromptMessage(text),
|
|
93
|
+
// the body we need to send to the request
|
|
94
|
+
requestBody = {
|
|
95
|
+
model: this.chatGptModel,
|
|
96
|
+
response_format: { type: 'json_object' },
|
|
97
|
+
messages: [
|
|
98
|
+
{
|
|
99
|
+
role: ChatGptMessage_1.ChatGptRole.SYSTEM,
|
|
100
|
+
content: this.FIND_TEXT_LANGUAGE_SYSTEM_ROLE,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
role: ChatGptMessage_1.ChatGptRole.USER,
|
|
104
|
+
content: translateText,
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
const response = yield this.request.post('', requestBody);
|
|
110
|
+
return {
|
|
111
|
+
firstChoice: this.getResponseFirstChoice(response),
|
|
112
|
+
usageTokens: this.getResponseUsageTokens(response),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
this.handleErrors(e);
|
|
117
|
+
return Promise.reject(e);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
getFindTextLanguagePromptMessage(text) {
|
|
122
|
+
return `In which language is this text in '${text}'? The response should be {'languageCode': 'language_code_here', 'isError': 'true or false based on whether you did find the language code or not'}.
|
|
123
|
+
Other responses or explanations are not allowed.`;
|
|
124
|
+
}
|
|
57
125
|
}
|
|
58
126
|
exports.TranslationService = TranslationService;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cobrowser/chatgpt",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "chatgpt services to connect our projects with chatgpt api",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chatgpt",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@cobrowser/logger": "^1.0.0",
|
|
25
25
|
"axios": "^1.6.1",
|
|
26
|
-
"axios-mock-adapter": "^1.22.0"
|
|
26
|
+
"axios-mock-adapter": "^1.22.0",
|
|
27
|
+
"openai": "^4.32.0"
|
|
27
28
|
},
|
|
28
29
|
"directories": {
|
|
29
30
|
"dist": "dist"
|
|
@@ -38,5 +39,5 @@
|
|
|
38
39
|
"bugs": {
|
|
39
40
|
"url": "https://bitbucket.org/cobrowser/cb_utils/issues"
|
|
40
41
|
},
|
|
41
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "5a4bff51323eed4e0c17a865e7783d8dfc0323e1"
|
|
42
43
|
}
|