-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
33 changed files
with
1,279 additions
and
187 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Knex } from 'knex' | ||
|
||
export async function up(knex: Knex): Promise<void> { | ||
return knex.schema.createTable('map_report', (table) => { | ||
table.increments('id').primary() | ||
table.string('locale_iso').notNullable() | ||
table.string('mode').notNullable() | ||
table.string('map').notNullable() | ||
table.datetime('timestamp').notNullable() | ||
table.text('prompt').notNullable() | ||
table.text('completion').notNullable() | ||
table.index(['locale_iso', 'mode', 'map']) | ||
}) | ||
} | ||
|
||
export async function down(knex: Knex): Promise<void> { | ||
return knex.schema.dropTable('map_report') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import Knex from 'knex' | ||
import MarkdownIt from 'markdown-it' | ||
import knexfile from '../knexfile' | ||
import { fetch, Agent } from 'undici' | ||
import ReportGeneratorService from '../services/ReportGeneratorService' | ||
import { BrawltimeKlickerService } from '~/plugins/klicker.service' | ||
import { publicProcedure, router } from '../trpc' | ||
import * as z from 'zod' | ||
import { TRPCError } from '@trpc/server' | ||
import { locales } from '~/locales' | ||
|
||
let reportGeneratorService: ReportGeneratorService | undefined | ||
|
||
const CUBE_URL = process.env.CUBE_URL | ||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY | ||
|
||
if (process.env.MYSQL_HOST && CUBE_URL != undefined && OPENAI_API_KEY != undefined) { | ||
const environment = process.env.NODE_ENV || 'development' | ||
const knex = Knex(knexfile[environment]) | ||
|
||
const agent = new Agent({ | ||
bodyTimeout: 30000, | ||
headersTimeout: 30000, | ||
}) | ||
|
||
const klickerService = new BrawltimeKlickerService(CUBE_URL, (url, input) => fetch(url as string, { | ||
...input as any, | ||
dispatcher: agent, | ||
}) as any) | ||
|
||
reportGeneratorService = new ReportGeneratorService(OPENAI_API_KEY, knex, klickerService) | ||
} else { | ||
if (CUBE_URL == undefined) { | ||
console.warn('CUBE_URL is not set, report generator will be unavailable') | ||
} | ||
if (OPENAI_API_KEY == undefined) { | ||
console.warn('OPENAI_API_KEY is not set, report generator will be unavailable') | ||
} | ||
if (process.env.MYSQL_HOST == undefined) { | ||
console.warn('MYSQL_HOST is not set, report generator will be unavailable') | ||
} | ||
} | ||
|
||
export async function updateAllReports() { | ||
if (reportGeneratorService == undefined) { | ||
console.warn('Report generator is not available') | ||
return { error: 'Report generator is not available' } | ||
} | ||
|
||
const activeEvents = await reportGeneratorService.getActiveEvents() | ||
console.log('Updating reports for active events') | ||
|
||
for (const activeEvent of activeEvents) { | ||
// TODO non-latin languages need more output tokens, exceeding the 4k output limit - chunk the prompt for them | ||
const latinLocales = ['de', 'en', 'it', 'pl', 'es'] // top 5 locales for the test phase | ||
for (const locale of locales.filter(l => latinLocales.includes(l.iso))) { | ||
await reportGeneratorService.updateReportsForMap(locale, activeEvent) | ||
console.log('Updated report for', locale.iso, activeEvent) | ||
} | ||
} | ||
|
||
return { events: activeEvents.length } | ||
} | ||
|
||
export async function updateReport(localeIso: string, mode: string, map: string) { | ||
if (reportGeneratorService == undefined) { | ||
console.warn('Report generator is not available') | ||
return { error: 'Report generator is not available' } | ||
} | ||
|
||
const locale = locales.find(l => l.iso == localeIso) | ||
|
||
if (locale == undefined) { | ||
return { error: 'Unknown locale' } | ||
} | ||
|
||
console.log('Updating reports for', { localeIso, mode, map }) | ||
|
||
await reportGeneratorService.updateReportsForMap(locale, { mode, map }) | ||
console.log('Updated report for', { localeIso, mode, map }) | ||
|
||
return { events: 1 } | ||
} | ||
|
||
export const reportRouter = router({ | ||
byModeMap: publicProcedure | ||
.input(z.object({ | ||
mode: z.string(), | ||
map: z.string(), | ||
localeIso: z.string(), | ||
})) | ||
.query(async ({ input, ctx }) => { | ||
if (reportGeneratorService == undefined) { | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Report generator is not available' | ||
}) | ||
} | ||
|
||
const report = await reportGeneratorService.getReportForMap(input.localeIso, input.mode, input.map) | ||
|
||
if (report == undefined) { | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'No report available' | ||
}) | ||
} | ||
|
||
const reportHtml = new MarkdownIt().render(report.markdown) | ||
|
||
// cache for 1 hour | ||
ctx.res.set('Cache-Control', 'public, max-age=3600') | ||
return { | ||
html: reportHtml, | ||
timestamp: report.timestamp, | ||
} | ||
}), | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import wtf from 'wtf_wikipedia' | ||
// @ts-ignore | ||
import wtfPluginApi from 'wtf-plugin-api' | ||
import { Cache } from '~/lib/cache' | ||
|
||
wtf.extend(wtfPluginApi) | ||
const wtfOpts = { domain: 'brawlstars.fandom.com', path: 'api.php' } | ||
|
||
const FANDOM_CACHE_MINUTES = 60 * 24 | ||
|
||
export interface FandomModeData { | ||
attribution: string | ||
shortDescription: string | ||
fullDescription: string | ||
tips: string[] | ||
brawlerTips: Record<string, string> | ||
} | ||
|
||
export default class FandomService { | ||
private modeCache = new Cache<string, FandomModeData|undefined>(FANDOM_CACHE_MINUTES) | ||
|
||
async cachedGetModeData(modeName: string) { | ||
return await this.modeCache.getOrUpdate(modeName, async () => { | ||
console.log('Updating fandom mode cache for', modeName) | ||
|
||
const data = await this.getModeData(modeName) | ||
if (data == undefined) { | ||
console.log('No fandom mode data for', modeName) | ||
} | ||
|
||
return data | ||
}) | ||
} | ||
|
||
private async getModeData(modeName: string): Promise<FandomModeData|undefined> { | ||
const modePage: any = await wtf.fetch(modeName, wtfOpts) | ||
if (modePage == null) { | ||
// does not exist | ||
return undefined | ||
} | ||
|
||
const description: string = modePage.section('').text() | ||
const shortDescription: string = description.split('\n')[0].replaceAll('"', '') | ||
const fullDescription: string = description.split('\n')[0] | ||
|
||
const brawlerTips: Record<string, string> = Object.fromEntries(modePage | ||
.section('Useful Brawlers') | ||
.list(0) | ||
.lines() | ||
.map((l: any) => l.text()) | ||
.map((l: string) => { | ||
const groups = l.match(/^(\w+): (.*)$/) | ||
if (groups == null) { | ||
return [] | ||
} | ||
return [groups[1], groups[2]] | ||
}) | ||
.filter((l: string[]) => l.length > 0) | ||
) | ||
|
||
const tips = modePage | ||
.section('Tips') | ||
.list(0) | ||
.lines() | ||
.map((l: any) => l.text()) | ||
|
||
return { | ||
attribution: modePage.url(), | ||
shortDescription, | ||
fullDescription, | ||
tips, | ||
brawlerTips, | ||
} | ||
} | ||
} |
Oops, something went wrong.