@miso.ai/doggoganger-api 0.0.1

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/lib/api/ask.js ADDED
@@ -0,0 +1,200 @@
1
+ import { trimObj } from '../utils.js';
2
+ import { answer, searchResults, questions, completions, utils } from '../data/index.js';
3
+
4
+ const CPS = 100;
5
+ const ITEMS_LOADING_TIME = 3;
6
+ const QUESTION_REVISED_TIME = 3;
7
+ const STAGES = [
8
+ {
9
+ name: 'fetch',
10
+ duration: 1.5,
11
+ text: `Checking the question and fetching results... `,
12
+ },
13
+ {
14
+ name: 'verify',
15
+ duration: 1.5,
16
+ text: `Verifying results... `,
17
+ },
18
+ {
19
+ name: 'generate',
20
+ duration: 1.5,
21
+ text: `Generating answer... `,
22
+ },
23
+ ];
24
+
25
+ const MODE_QUESTION = 0;
26
+ const MODE_SEARCH = 1;
27
+
28
+ export default class Ask {
29
+
30
+ constructor(options = {}) {
31
+ this._options = options;
32
+ this._answers = new Map();
33
+ }
34
+
35
+ questions(payload, options = {}) {
36
+ const answer = this._createAnswer(MODE_QUESTION, payload, options);
37
+ const { question_id } = answer;
38
+ return { question_id };
39
+ }
40
+
41
+ search(payload, options = {}) {
42
+ const miso_id = utils.uuid();
43
+ const result = { miso_id, ...searchResults(payload) };
44
+ if (payload.answer === undefined || payload.answer) {
45
+ result.question_id = this._createAnswer(MODE_SEARCH, payload, options).question_id;
46
+ }
47
+ return result;
48
+ }
49
+
50
+ autocomplete({ q, completion_fields = ['title'], rows = 5 }) {
51
+ return {
52
+ completions: completions({ q, completion_fields, rows }),
53
+ };
54
+ }
55
+
56
+ search_autocomplete(args) {
57
+ return this.autocomplete(args);
58
+ }
59
+
60
+ _createAnswer(mode, payload, options = {}) {
61
+ const answer = new Answer(mode, payload, { ...this._options, ...options });
62
+ this._answers.set(answer.question_id, answer);
63
+ return answer;
64
+ }
65
+
66
+ answer(questionId) {
67
+ const answer = this._answers.get(questionId);
68
+ if (!answer) {
69
+ const error = new Error(`Question not found: ${questionId}`);
70
+ error.status = 404;
71
+ throw error;
72
+ }
73
+ return answer.get();
74
+ }
75
+
76
+ related_questions(payload) {
77
+ const miso_id = utils.uuid();
78
+ return {
79
+ related_questions: [...questions(payload)],
80
+ miso_id,
81
+ };
82
+ }
83
+
84
+ trending_questions(payload) {
85
+ return this.related_questions(payload);
86
+ }
87
+
88
+ }
89
+
90
+ class Answer {
91
+
92
+ constructor(mode, payload, { answerFormat, answerSampling, answerLanguages, ...options } = {}) {
93
+ this._mode = mode;
94
+ this._options = Object.freeze(options);
95
+ const timestamp = this.timestamp = Date.now();
96
+ this._data = answer({ ...payload, timestamp }, { answerFormat, answerSampling, answerLanguages });
97
+ }
98
+
99
+ get question_id() {
100
+ return this._data.question_id;
101
+ }
102
+
103
+ get searchResults() {
104
+ const { products, total, facet_counts } = this._data;
105
+ return trimObj({ products, total, facet_counts });
106
+ }
107
+
108
+ get() {
109
+ const now = Date.now();
110
+ const elapsed = (now - this.timestamp) * (this._options.speedRate || 1) / 1000;
111
+ const question = this._question(elapsed);
112
+ const [answer_stage, answer, finished, revision] = this._answer(elapsed);
113
+ const sources = this._sources(elapsed, finished);
114
+ const related_resources = this._related_resources(elapsed, finished);
115
+ const followup_questions = this._followup_questions(elapsed, finished);
116
+ const { question_id, datetime, parent_question_id, images } = this._data;
117
+
118
+ switch (this._mode) {
119
+ case MODE_QUESTION:
120
+ return {
121
+ answer,
122
+ answer_stage,
123
+ datetime,
124
+ finished,
125
+ revision,
126
+ parent_question_id,
127
+ question,
128
+ question_id,
129
+ images,
130
+ sources,
131
+ related_resources,
132
+ followup_questions,
133
+ };
134
+ case MODE_SEARCH:
135
+ return {
136
+ answer,
137
+ answer_stage,
138
+ datetime,
139
+ finished,
140
+ revision,
141
+ question_id,
142
+ images,
143
+ sources,
144
+ };
145
+ default:
146
+ throw new Error(`Unknown mode: ${this._mode}`);
147
+ }
148
+ }
149
+
150
+ _question(elapsed) {
151
+ const { question } = this._data;
152
+ return elapsed > QUESTION_REVISED_TIME ? `${question} [revised]` : question;
153
+ }
154
+
155
+ _answer(elapsed) {
156
+ let elapsedInStage = elapsed;
157
+ for (const stage of STAGES) {
158
+ elapsedInStage -= stage.duration;
159
+ if (elapsedInStage < 0) {
160
+ return [stage.name, stage.text, false, elapsed];
161
+ }
162
+ }
163
+ const { answer } = this._data;
164
+ const length = Math.floor(elapsedInStage * CPS);
165
+ const finished = length >= answer.length;
166
+ const text = finished ? answer : answer.slice(0, length);
167
+ return ['result', text, finished, elapsed];
168
+ }
169
+
170
+ _sources(elapsed, finished) {
171
+ const { sources } = this._data;
172
+ if (finished) {
173
+ return sources;
174
+ }
175
+ const { length } = sources;
176
+ const loaded = Math.floor(length * elapsed / ITEMS_LOADING_TIME);
177
+ return sources.slice(0, loaded);
178
+ }
179
+
180
+ _related_resources(elapsed, finished) {
181
+ const { related_resources } = this._data;
182
+ if (finished) {
183
+ return related_resources;
184
+ }
185
+ const { length } = related_resources;
186
+ const loaded = Math.floor(length * elapsed / ITEMS_LOADING_TIME);
187
+ return related_resources.slice(0, loaded);
188
+ }
189
+
190
+ _followup_questions(elapsed, finished) {
191
+ const { followup_questions } = this._data;
192
+ if (finished || !followup_questions) {
193
+ return followup_questions;
194
+ }
195
+ const { length } = followup_questions;
196
+ const loaded = Math.floor(length * elapsed / ITEMS_LOADING_TIME);
197
+ return followup_questions.slice(0, loaded);
198
+ }
199
+
200
+ }
@@ -0,0 +1,17 @@
1
+ import Ask from './ask.js';
2
+ import Search from './search.js';
3
+ import Recommendation from './recommendation.js';
4
+ import Interactions from './interactions.js';
5
+ import Products from './products.js';
6
+
7
+ export default class Api {
8
+
9
+ constructor(options) {
10
+ this.ask = new Ask(options);
11
+ this.search = new Search(options);
12
+ this.recommendation = new Recommendation(options);
13
+ this.interactions = new Interactions(options);
14
+ this.products = new Products(options);
15
+ }
16
+
17
+ }
@@ -0,0 +1,13 @@
1
+ export default class Interactions {
2
+
3
+ constructor(options) {
4
+ this._options = options;
5
+ }
6
+
7
+ upload(records) {
8
+ return [];
9
+ }
10
+
11
+ // TODO: delete
12
+
13
+ }
@@ -0,0 +1,32 @@
1
+ export default class Products {
2
+
3
+ constructor(options) {
4
+ this._options = options;
5
+ }
6
+
7
+ upload(records) {
8
+ return [];
9
+ }
10
+
11
+ batchDelete(ids) {
12
+ return [];
13
+ }
14
+
15
+ ids() {
16
+ const ids = [];
17
+ for (let i = 0; i < 5000; i++) {
18
+ ids.push(mockProductId(i));
19
+ }
20
+ return {
21
+ ids,
22
+ };
23
+ }
24
+
25
+ // TODO: status
26
+
27
+ }
28
+
29
+ function mockProductId(i) {
30
+ const prefix = i < 10 ? 'p_000' : i < 100 ? 'p_00' : i < 1000 ? 'p_0' : 'p_';
31
+ return `${prefix}${i}`;
32
+ }
@@ -0,0 +1,21 @@
1
+ import { products } from '../data/index.js';
2
+
3
+ export default class Recommendation {
4
+
5
+ constructor(options) {
6
+ this._options = options;
7
+ }
8
+
9
+ user_to_products({ rows = 5 }) {
10
+ return {
11
+ products: [...products({ rows })],
12
+ };
13
+ }
14
+
15
+ product_to_products({ rows = 5 }) {
16
+ return {
17
+ products: [...products({ rows })],
18
+ };
19
+ }
20
+
21
+ }
@@ -0,0 +1,21 @@
1
+ import { products, completions } from '../data/index.js';
2
+
3
+ export default class Search {
4
+
5
+ constructor(options) {
6
+ this._options = options;
7
+ }
8
+
9
+ search({ rows = 5 }) {
10
+ return {
11
+ products: [...products({ rows })],
12
+ };
13
+ }
14
+
15
+ autocomplete({ q, completion_fields = ['title'], rows = 5 }) {
16
+ return {
17
+ completions: completions({ q, completion_fields, rows }),
18
+ };
19
+ }
20
+
21
+ }
@@ -0,0 +1,69 @@
1
+ import { fields, utils } from '@miso.ai/lorem';
2
+ import { articles } from './articles.js';
3
+ import { images as _images } from './images.js';
4
+ import { questions } from './questions.js';
5
+ import { facets as generateFacetFields } from './facets.js';
6
+
7
+ const { randomInt, formatDatetime, sample, uuid, shuffle, excludeHtml } = utils;
8
+
9
+ export function answer({
10
+ _ctrl = {},
11
+ question,
12
+ parent_question_id,
13
+ fl = ['cover_image', 'url'],
14
+ source_fl = ['cover_image', 'url'],
15
+ related_resource_fl = ['cover_image', 'url'],
16
+ cite_link = false,
17
+ cite_start = '[',
18
+ cite_end = ']',
19
+ rows = 10,
20
+ facets,
21
+ timestamp = Date.now(),
22
+ _meta: {
23
+ page = 0,
24
+ } = {},
25
+ }, { answerFormat = 'markdown', answerSampling, answerLanguages = [] } = {}) {
26
+
27
+ const question_id = uuid();
28
+ const datetime = formatDatetime(timestamp);
29
+
30
+ const sampling = answerSampling !== undefined ? Math.max(0, Math.min(1, answerSampling)) : undefined;
31
+ const features = answerLanguages.length ? answerLanguages.map(language => `lang-${language}`) : undefined;
32
+
33
+ const related_resources = [...articles({ rows: sampleRandomInt(6, 8, sampling), fl: related_resource_fl })].map(excludeHtml);
34
+ const images = [..._images({ rows: sampleRandomInt(2, 12, sampling) })];
35
+ const sources = [...articles({ rows: sampleRandomInt(4, 6, sampling), fl: source_fl })].map(excludeHtml);
36
+
37
+ const total = _ctrl.total !== undefined ? _ctrl.total : randomInt(1000, 10000);
38
+ const products = [...articles({ rows: Math.min(total - page * rows, rows), fl })].map(excludeHtml);
39
+
40
+ const facet_counts = facets ? { facet_fields: generateFacetFields({ facets, _ctrl }) } : undefined;
41
+
42
+ const citation = {
43
+ link: cite_link !== '0' && !!cite_link,
44
+ start: cite_start,
45
+ end: cite_end,
46
+ unused: shuffle([...Array(sources.length).keys()]),
47
+ };
48
+ const answer = fields.answer({ sources, citation, format: answerFormat, sampling, features });
49
+ const followup_questions = [...questions({ rows: sampleRandomInt(3, 6) })];
50
+
51
+ return {
52
+ question,
53
+ question_id,
54
+ ...(parent_question_id && { parent_question_id }),
55
+ datetime,
56
+ answer,
57
+ images,
58
+ sources,
59
+ products,
60
+ total,
61
+ facet_counts,
62
+ related_resources,
63
+ followup_questions,
64
+ };
65
+ }
66
+
67
+ function sampleRandomInt(min, max, sampling) {
68
+ return randomInt(sample(min, sampling), sample(max, sampling));
69
+ }
@@ -0,0 +1,50 @@
1
+ import { fields, utils } from '@miso.ai/lorem';
2
+
3
+ const FIELDS = new Set([
4
+ 'cover_image',
5
+ 'url',
6
+ 'created_at',
7
+ 'updated_at',
8
+ 'published_at',
9
+ ]);
10
+
11
+ export function *articles({ rows, ...options } = {}) {
12
+ for (let i = 0; i < rows; i++) {
13
+ yield article({ ...options, index: i });
14
+ }
15
+ }
16
+
17
+ function article({ html, fl = [] } = {}) {
18
+ const id = utils.id();
19
+
20
+ const article = {
21
+ product_id: id,
22
+ authors: fields.authors(),
23
+ categories: [],
24
+ tags: fields.tags(),
25
+ title: fields.title({ size: [4, 10] }),
26
+ snippet: fields.description({ size: [20, 40] }),
27
+ html: fields.html(html),
28
+ };
29
+
30
+ for (const field of fl) {
31
+ if (FIELDS.has(field)) {
32
+ article[field] = property(article, field);
33
+ }
34
+ }
35
+
36
+ return article;
37
+ }
38
+
39
+ function property({ product_id }, field) {
40
+ switch (field) {
41
+ case 'cover_image':
42
+ return fields.image();
43
+ case 'url':
44
+ return `/products/${product_id}`;
45
+ case 'created_at':
46
+ case 'updated_at':
47
+ case 'published_at':
48
+ return fields.date();
49
+ }
50
+ }
@@ -0,0 +1,37 @@
1
+ import { fields, utils } from '@miso.ai/lorem';
2
+
3
+ export function completions({ q, completion_fields, rows } = {}) {
4
+ let index = 0;
5
+ const result = {};
6
+ for (const field of completion_fields) {
7
+ result[field] = completionsForField(field, q, index, rows);
8
+ index += rows;
9
+ }
10
+ return result;
11
+ }
12
+
13
+ function completionsForField(field, q, index, rows) {
14
+ const completions = [];
15
+ for (let i = 0; i < rows; i++) {
16
+ completions.push(completion(field, q, index));
17
+ }
18
+ return completions;
19
+ }
20
+
21
+ function completion(field, q, index) {
22
+ const text = fields.title();
23
+ const i = utils.randomInt(0, text.length);
24
+ const prefix = text.substring(0, i);
25
+ const suffix = text.substring(i);
26
+ return {
27
+ text: `${prefix}${q}${suffix}`,
28
+ text_with_markups: `${marked(prefix)}${q}${marked(suffix)}`,
29
+ text_with_inverted_markups: `${prefix}${marked(q)}${suffix}`,
30
+ _field: field,
31
+ _index: index,
32
+ };
33
+ }
34
+
35
+ function marked(text) {
36
+ return text.length > 0 ? `<mark>${text}</mark>` : '';
37
+ }
@@ -0,0 +1,58 @@
1
+ import { fields } from '@miso.ai/lorem';
2
+
3
+ const DEFAULT_FACET_SIZE = 5;
4
+ const MAX_FACET_SIZE = 10;
5
+ const MIN_FACET_SIZE = 1;
6
+
7
+ const HIGHEST_FACET_COUNT_RATIO_RANGE = [1000, 5000];
8
+ const NEXT_FACET_COUNT_RATIO_RANGE = [0.75, 0.25];
9
+
10
+ export function facets({ facets, ...options } = {}) {
11
+ const results = {};
12
+ for (let facet of facets) {
13
+ if (typeof facet === 'string') {
14
+ facet = { field: facet };
15
+ }
16
+ results[facet.field] = facetCountList(facet, options);
17
+ }
18
+ return results;
19
+ }
20
+
21
+ function facetCountList(facet, { _ctrl = {}, ...options } = {}) {
22
+ let { field, size = DEFAULT_FACET_SIZE } = facet;
23
+ size = _ctrl.total === 0 ? 0 : Math.max(MIN_FACET_SIZE, Math.min(size, MAX_FACET_SIZE));
24
+ let count = highestFacetCountValue(field, options);
25
+
26
+ const usedTerms = new Set();
27
+ const results = [];
28
+ for (let i = 0; i < size; i++) {
29
+ results.push([ getTerm(field, usedTerms), count ]);
30
+ count = nextFacetCountValue(count, field, options);
31
+ }
32
+ return results;
33
+ }
34
+
35
+ function getTerm(field, usedTerms) {
36
+ if (usedTerms.size > 50) {
37
+ return fields.term({ field });
38
+ }
39
+ while (true) {
40
+ const term = fields.term({ field });
41
+ if (!usedTerms.has(term)) {
42
+ usedTerms.add(term);
43
+ return term;
44
+ }
45
+ }
46
+ }
47
+
48
+ function highestFacetCountValue(field, options) {
49
+ // TODO: capped at _ctrl.total
50
+ const [min, max] = HIGHEST_FACET_COUNT_RATIO_RANGE;
51
+ return Math.ceil(Math.random() * (max - min) + min);
52
+ }
53
+
54
+ function nextFacetCountValue(value, field, options) {
55
+ const [min, max] = NEXT_FACET_COUNT_RATIO_RANGE;
56
+ const ratio = Math.random() * (max - min) + min;
57
+ return Math.ceil(value * ratio);
58
+ }
@@ -0,0 +1,22 @@
1
+ import { fields, utils } from '@miso.ai/lorem';
2
+
3
+ export function *images({ rows, ...options } = {}) {
4
+ for (let i = 0; i < rows; i++) {
5
+ yield image({ ...options, index: i });
6
+ }
7
+ }
8
+
9
+ function image({ fl = [] } = {}) {
10
+ const id = utils.id();
11
+
12
+ return {
13
+ product_id: id,
14
+ image_src: fields.image({ size: [1200, 400] }),
15
+ image_alt: fields.title({ size: [2, 4] }),
16
+ title: fields.title({ size: [4, 10] }),
17
+ url: `/products/${id}`,
18
+ created_at: fields.date(),
19
+ updated_at: fields.date(),
20
+ published_at: fields.date(),
21
+ };
22
+ }
@@ -0,0 +1,8 @@
1
+ export * from '@miso.ai/lorem';
2
+ export * from './products.js';
3
+ export * from './articles.js';
4
+ export * from './questions.js';
5
+ export * from './answers.js';
6
+ export * from './search.js';
7
+ export * from './completions.js';
8
+ export * from './images.js';
@@ -0,0 +1,29 @@
1
+ import { fields, utils } from '@miso.ai/lorem';
2
+
3
+ export function *products({ rows, ...options } = {}) {
4
+ for (let i = 0; i < rows; i++) {
5
+ yield product({ ...options, index: i });
6
+ }
7
+ }
8
+
9
+ function product({} = {}) {
10
+ const id = utils.id();
11
+ const prices = utils.repeat(fields.price, [1, 2]);
12
+ prices.sort();
13
+
14
+ return {
15
+ product_id: id,
16
+ authors: fields.authors(),
17
+ categories: [],
18
+ tags: fields.tags(),
19
+ title: fields.title(),
20
+ description: fields.description(),
21
+ //html,
22
+ cover_image: fields.image(),
23
+ url: `https://dummy.miso.ai/products/${id}`,
24
+ sale_price: prices[0],
25
+ original_price: prices[prices.length - 1],
26
+ rating: fields.rating(),
27
+ availability: fields.availability(),
28
+ };
29
+ }
@@ -0,0 +1,11 @@
1
+ import { fields } from '@miso.ai/lorem';
2
+
3
+ export function *questions({ rows = 5, ...options } = {}) {
4
+ for (let i = 0; i < rows; i++) {
5
+ yield question({ ...options, index: i });
6
+ }
7
+ }
8
+
9
+ function question({} = {}) {
10
+ return fields.description({ size: [4, 8], punctuation: '?' });
11
+ }
@@ -0,0 +1,24 @@
1
+ import { utils } from '@miso.ai/lorem';
2
+ import { articles } from './articles.js';
3
+ import { facets as generateFacetFields } from './facets.js';
4
+
5
+ export function searchResults({
6
+ _ctrl = {},
7
+ q,
8
+ fl = ['cover_image', 'url'],
9
+ rows = 10,
10
+ facets,
11
+ _meta: {
12
+ page = 0,
13
+ } = {},
14
+ }) {
15
+ const total = _ctrl.total !== undefined ? _ctrl.total : utils.randomInt(1000, 10000);
16
+ const products = [...articles({ rows: Math.min(total - page * rows, rows), fl })].map(utils.excludeHtml);
17
+ const facet_counts = facets ? { facet_fields: generateFacetFields({ facets, _ctrl }) } : undefined;
18
+
19
+ return {
20
+ products,
21
+ total,
22
+ facet_counts,
23
+ };
24
+ }
package/lib/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { default as Api } from './api/index.js';
2
+ import * as data from './data/index.js';
3
+
4
+ export { data };
package/lib/utils.js ADDED
@@ -0,0 +1,12 @@
1
+ export function trimObj(obj) {
2
+ if (typeof obj !== 'object') {
3
+ return obj;
4
+ }
5
+ const trimmed = {};
6
+ for (const k in obj) {
7
+ if (Object.prototype.hasOwnProperty.call(obj, k) && obj[k] !== undefined) {
8
+ trimmed[k] = obj[k];
9
+ }
10
+ }
11
+ return trimmed;
12
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@miso.ai/doggoganger-api",
3
+ "description": "A dummy Miso API module",
4
+ "type": "module",
5
+ "main": "lib/index.js",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {},
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/MisoAI/doggoganger.git"
13
+ },
14
+ "homepage": "https://github.com/MisoAI/doggoganger/",
15
+ "license": "MIT",
16
+ "contributors": [
17
+ "simonpai <simon.pai@askmiso.com>"
18
+ ],
19
+ "bugs": {
20
+ "url": "https://github.com/MisoAI/doggoganger/issues"
21
+ },
22
+ "dependencies": {
23
+ "@miso.ai/lorem": "0.0.1"
24
+ },
25
+ "version": "0.0.1"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@miso.ai/doggoganger-api",
3
+ "description": "A dummy Miso API module",
4
+ "type": "module",
5
+ "main": "lib/index.js",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/MisoAI/doggoganger.git"
14
+ },
15
+ "homepage": "https://github.com/MisoAI/doggoganger/",
16
+ "license": "MIT",
17
+ "contributors": [
18
+ "simonpai <simon.pai@askmiso.com>"
19
+ ],
20
+ "bugs": {
21
+ "url": "https://github.com/MisoAI/doggoganger/issues"
22
+ },
23
+ "dependencies": {
24
+ "@miso.ai/lorem": "file:packages/lorem"
25
+ }
26
+ }