• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cofacts / rumors-api / 5892775065

17 Aug 2023 03:17PM UTC coverage: 88.141% (-0.08%) from 88.218%
5892775065

push

github

web-flow
Merge pull request #312 from cofacts/compress-logs

feat: record less bytes in GraphQL request

707 of 855 branches covered (82.69%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

1441 of 1582 relevant lines covered (91.09%)

22.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.65
/src/graphql/mutations/CreateAIReply.js
1
import { GraphQLString, GraphQLNonNull } from 'graphql';
2

3
import openai from 'util/openai';
4
import { assertUser } from 'util/user';
5
import client from 'util/client';
6
import delayForMs from 'util/delayForMs';
7
import { AIReply } from 'graphql/models/AIResponse';
8

9
const monthFormatter = Intl.DateTimeFormat('zh-TW', {
31✔
10
  year: 'numeric',
11
  month: 'long',
12
  timeZone: 'Asia/Taipei',
13
});
14

15
/**
16
 * Create an new AIReply, initially in LOADING state, then becomes ERROR or SUCCESS,
17
 * and returns the AI reply.
18
 * If there is no enough content for AI, it resolves to null.
19
 */
20
export async function createNewAIReply({
21
  article,
22
  user,
23
  completionOptions = {},
4✔
24
}) {
25
  // article.hyperlinks deduped by URL.
26
  const dedupedHyperlinks = Object.values(
4✔
27
    (article.hyperlinks ?? []).reduce((map, hyperlink) => {
6✔
28
      if (
6✔
29
        !map[hyperlink.url] ||
8✔
30
        /* hyperlink exists, but fetch failed */ !map[hyperlink.url].title
31
      ) {
32
        map[hyperlink.url] = hyperlink;
5✔
33
      }
34
      return map;
6✔
35
    }, {})
36
  );
37

38
  /**
39
   * Determine if article has no content by replacing all URLs with its scrapped content.
40
   * This will become empty string if and only if:
41
   * - The article only contains URLs, no other text, and
42
   * - All URL scrapping results fail (no title, no summary)
43
   *
44
   * Abort AI reply generation in this case.
45
   */
46
  const replacedArticleText = dedupedHyperlinks
4✔
47
    .reduce(
48
      (text, { url, title, summary }) =>
49
        text.replaceAll(url, `${title} ${summary}`),
4✔
50
      article.text
51
    )
52
    .trim();
53

54
  if (replacedArticleText.length === 0) return null;
4✔
55

56
  // Argumenting hyperlinks with summary and titles
57
  const argumentedArticleText = dedupedHyperlinks.reduce(
3✔
58
    (text, { url, title, summary }) =>
59
      title
2✔
60
        ? text.replaceAll(url, `[${title} ${summary}](${url})`)
61
        : /* Fetch failed, don't replace */ text,
62
    article.text
63
  );
64

65
  const thisMonthParts = monthFormatter.formatToParts(new Date());
3✔
66
  const thisYearStr = thisMonthParts.find(p => p.type === 'year').value;
3✔
67
  const thisROCYearStr = (+thisYearStr - 1911).toString();
3✔
68
  const thisMonthStr = thisMonthParts.find(p => p.type === 'month').value;
9✔
69
  const createdMonth = monthFormatter.format(new Date(article.createdAt));
3✔
70

71
  const completionRequest = {
3✔
72
    model: 'gpt-3.5-turbo',
73
    messages: [
74
      {
75
        role: 'system',
76
        content: `現在是${thisYearStr}年(民國${thisROCYearStr}年)${thisMonthStr}月。你是協助讀者進行媒體識讀的小幫手。你說話時總是使用台灣繁體中文。有讀者傳了一則網路訊息給你。這則訊息${createdMonth}就在網路上流傳。`,
77
      },
78
      {
79
        role: 'user',
80
        content: argumentedArticleText,
81
      },
82
      {
83
        role: 'user',
84
        content:
85
          '請問作為閱聽人,我應該注意這則訊息的哪些地方呢?請節錄訊息中需要特別留意或懷疑的地方,說明為何閱聽人需要注意它。請只就以上內文回應,不要編造。謝謝',
86
      },
87
    ],
88
    user: user.id,
89
    temperature: 0,
90
    ...completionOptions,
91
  };
92

93
  const newResponse = {
3✔
94
    userId: user.id,
95
    appId: user.appId,
96
    docId: article.id,
97
    type: 'AI_REPLY',
98
    status: 'LOADING',
99
    request: JSON.stringify(completionRequest),
100
    createdAt: new Date(),
101
  };
102

103
  // Resolves to loading AI Response.
104
  const newResponseIdPromise = client
3✔
105
    .index({
106
      index: 'airesponses',
107
      type: 'doc',
108
      body: newResponse,
109
    })
110
    .then(({ body: { result, _id } }) => {
111
      /* istanbul ignore if */
112
      if (result !== 'created') {
3✔
113
        throw new Error(`Cannot create AI reply: ${result}`);
114
      }
115
      return _id;
3✔
116
    });
117

118
  const openAIResponsePromise = openai
3✔
119
    .createChatCompletion(completionRequest)
120
    .then(({ data }) => data)
2✔
121
    .catch(error => {
122
      console.error(error);
1✔
123

124
      /* Resolve with Error instance, which will be used to update AI response below */
125
      /* istanbul ignore else */
126
      if (error instanceof Error) return error;
1✔
127
      return new Error(error);
×
128
    });
129

130
  // Resolves to completed or errored AI response.
131
  return Promise.all([openAIResponsePromise, newResponseIdPromise])
3✔
132
    .then(([apiResult, aiResponseId]) =>
133
      // Update using aiResponse._id according to apiResult
134
      client.update({
3✔
135
        index: 'airesponses',
136
        type: 'doc',
137
        id: aiResponseId,
138
        _source: true,
139
        body: {
140
          doc:
141
            apiResult instanceof Error
3✔
142
              ? {
143
                  status: 'ERROR',
144
                  text: apiResult.toString(),
145
                  updatedAt: new Date(),
146
                }
147
              : {
148
                  status: 'SUCCESS',
149
                  text: apiResult.choices[0].message.content,
150
                  ...(apiResult.usage
2!
151
                    ? {
152
                        usage: {
153
                          promptTokens: apiResult.usage.prompt_tokens,
154
                          completionTokens: apiResult.usage.completion_tokens,
155
                          totalTokens: apiResult.usage.total_tokens,
156
                        },
157
                      }
158
                    : undefined),
159
                  updatedAt: new Date(),
160
                },
161
        },
162
      })
163
    )
164
    .then(({ body: { _id, get: { _source } } }) => ({ id: _id, ..._source }));
3✔
165
}
166

167
export default {
168
  type: AIReply,
169
  description:
170
    'Create an AI reply for a specific article. If existed, returns an existing one. If information in the article is not sufficient for AI, return null.',
171
  args: {
172
    articleId: { type: new GraphQLNonNull(GraphQLString) },
173
  },
174
  async resolve(rootValue, { articleId }, { loaders, user }) {
175
    assertUser(user);
7✔
176

177
    const article = await loaders.docLoader.load({
7✔
178
      index: 'articles',
179
      id: articleId,
180
    });
181

182
    if (!article) throw new Error(`Article ${articleId} does not exist.`);
7✔
183

184
    // Try reading successful AI response.
185
    // Break the loop when there is no latest loading AI response.
186
    //
187
    for (;;) {
6✔
188
      // First, find latest successful airesponse. Return if found.
189
      //
190
      const {
191
        body: {
192
          hits: {
193
            hits: [successfulAiResponse],
194
          },
195
        },
196
      } = await client.search({
6✔
197
        index: 'airesponses',
198
        type: 'doc',
199
        body: {
200
          query: {
201
            bool: {
202
              must: [
203
                { term: { type: 'AI_REPLY' } },
204
                { term: { docId: articleId } },
205
                { term: { status: 'SUCCESS' } },
206
              ],
207
            },
208
          },
209
          sort: {
210
            createdAt: 'desc',
211
          },
212
          size: 1,
213
        },
214
      });
215

216
      if (successfulAiResponse) {
6✔
217
        return {
2✔
218
          id: successfulAiResponse._id,
219
          ...successfulAiResponse._source,
220
        };
221
      }
222

223
      // If no successful AI responses, find loading responses created within 1 min.
224
      //
225
      const {
226
        body: { count },
227
      } = await client.count({
4✔
228
        index: 'airesponses',
229
        type: 'doc',
230
        body: {
231
          query: {
232
            bool: {
233
              must: [
234
                { term: { type: 'AI_REPLY' } },
235
                { term: { docId: articleId } },
236
                { term: { status: 'LOADING' } },
237
                {
238
                  // loading document created within 1 min
239
                  range: {
240
                    createdAt: {
241
                      gte: 'now-1m',
242
                    },
243
                  },
244
                },
245
              ],
246
            },
247
          },
248
        },
249
      });
250

251
      // No AI response available now, break the loop and try create.
252
      //
253
      if (count === 0) {
4!
254
        break;
4✔
255
      }
256

257
      // Wait a bit to search for successful AI response again.
258
      // If there are any loading AI response becomes successful during the wait,
259
      // it will be picked up when the loop is re-entered.
260
      await delayForMs(1000);
×
261
    }
262

263
    return createNewAIReply({ article, user });
4✔
264
  },
265
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc