Skip to content

Commit

Permalink
feat: WIP add anime adventure plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
transitive-bullshit committed Apr 16, 2023
1 parent a48bee8 commit 8340c46
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 1 deletion.
1 change: 1 addition & 0 deletions examples/anime-adventure/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wrangler.toml
39 changes: 39 additions & 0 deletions examples/anime-adventure/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "chatgpt-plugin-example-anime-adventure",
"version": "0.1.2",
"private": true,
"description": "A neverending choose-your-own-adventure Anime game.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"repository": "transitive-bullshit/chatgpt-plugin-ts",
"license": "MIT",
"type": "module",
"engines": {
"node": ">=14"
},
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler publish"
},
"dependencies": {
"@cloudflare/itty-router-openapi": "^0.0.15",
"anilist-node": "^1.13.2",
"chatgpt-plugin": "workspace:../../packages/chatgpt-plugin",
"zod": "^3.21.4"
},
"aiPlugin": {
"name": "Anime Adventure",
"description_for_model": "Plugin which enables the user to play a neverending choose-your-own-adventure Anime game. Use this plugin to generate a story about an anime of the user's choosing. The game starts with the \"start\" command, and the user can then use the \"continue\" command to continue the story. The user can also use the \"restart\" command to start a new story.",
"logo_url": "TODO",
"contact_email": "travis@transitivebullsh.it",
"legal_info_url": "https://transitivebullsh.it"
},
"keywords": [
"openai",
"chatgpt",
"plugin",
"openapi",
"anime",
"game",
"choose your own adventure"
]
}
18 changes: 18 additions & 0 deletions examples/anime-adventure/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# ChatGPT Plugin Example - Anime Adventure <!-- omit in toc -->

> TODO: WIP
[![Build Status](https://github.com/transitive-bullshit/chatgpt-plugin-ts/actions/workflows/test.yml/badge.svg)](https://github.com/transitive-bullshit/chatgpt-plugin-ts/actions/workflows/test.yml) [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/transitive-bullshit/chatgpt-plugin-ts/blob/main/license) [![Prettier Code Formatting](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io)

- [Intro](#intro)
- [License](#license)

## Intro

TODO: WIP

## License

MIT © [Travis Fischer](https://transitivebullsh.it)

If you found this project interesting, please consider [sponsoring me](https://github.com/sponsors/transitive-bullshit) or <a href="https://twitter.com/transitive_bs">following me on twitter <img src="https://storage.googleapis.com/saasify-assets/twitter-logo.svg" alt="twitter" height="24px" align="center"></a>
47 changes: 47 additions & 0 deletions examples/anime-adventure/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { OpenAPIRouter } from '@cloudflare/itty-router-openapi'
import { defineAIPluginManifest } from 'chatgpt-plugin'

import * as routes from './routes'
import pkg from '../package.json'

export interface Env {
ANILIST_ACCESS_TOKEN: string
ENVIRONMENT: string
}

const router = OpenAPIRouter({
schema: {
info: {
title: pkg.aiPlugin.name,
version: pkg.version
}
}
})

router.get('/start', routes.StartGame)

router.get('/.well-known/ai-plugin.json', (request: Request) => {
const host = request.headers.get('host')
const pluginManifest = defineAIPluginManifest(
{
description_for_human: pkg.description,
name_for_human: pkg.aiPlugin.name,
...pkg.aiPlugin
},
{ openAPIUrl: `https://${host}/openapi.json` }
)

return new Response(JSON.stringify(pluginManifest, null, 2), {
headers: {
'content-type': 'application/json;charset=UTF-8'
}
})
})

// 404 for everything else
router.all('*', () => new Response('Not Found.', { status: 404 }))

export default {
fetch: (request: Request, env: Env, ctx: ExecutionContext) =>
router.handle(request, env, ctx)
}
115 changes: 115 additions & 0 deletions examples/anime-adventure/src/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { OpenAPIRoute, Query, Str } from '@cloudflare/itty-router-openapi'
import Anilist from 'anilist-node'

// import { isValidChatGPTIPAddress } from 'chatgpt-plugin'
import * as types from './types'
import * as utils from './utils'

export class StartGame extends OpenAPIRoute {
static schema = {
summary:
'Starts a new game of Anime Adventure. The user must provide the name of an Anime that they want to base the game around. You should use the anime metadata that is returned to start a story set in the world of this anime.',
parameters: {
anime: Query(
new Str({
description: 'Name of the anime show',
example: 'cowboy bebop'
}),
{
required: true
}
)
},
responses: {
'200': {
schema: {
anime: {
id: new Str(),
title: new Str({
description: 'Official title of the anime'
}),

description: new Str({
description: "Overview of the show and it's plot"
}),

genres: [new Str()],
tags: [new Str()],
characters: [new Str()],
reviews: [
{
summary: new Str(),
body: new Str()
}
]
}
}
}
}
}

async handle(request: Request, env: any, _ctx, data: Record<string, any>) {
const anilistAccessToken = env.ANILIST_ACCESS_TOKEN
if (!anilistAccessToken) {
return new Response('ANILIST_ACCESS_TOKEN not set', { status: 500 })
}

const ip = request.headers.get('Cf-Connecting-Ip')
if (!ip) {
console.warn('search error missing IP address')
return new Response('invalid source IP', { status: 500 })
}

// TODO: protect against abuse in prod
// if (env.ENVIRONMENT === 'production' && !isValidChatGPTIPAddress(ip)) {
// // console.warn('search error invalid IP address', ip)
// return new Response(`Forbidden`, { status: 403 })
// }

const openaiUserLocaleInfo = request.headers.get(
'openai-subdivision-1-iso-code'
)
const { anime: query } = data
console.log()
console.log()
console.log('>>> search', `${query} (${openaiUserLocaleInfo}, ${ip})`)
console.log()

const anilist = new Anilist(anilistAccessToken)
const r = (await anilist.search('anime', query)) as types.AnimeSearchResults

if (!r.media?.length) {
return new Response(`Anime "${query}" not found`, { status: 404 })
}

const media = r.media[0]
const anime = await anilist.media.anime(media.id)

const animeMetadata = {
...utils.pick(anime, 'id', 'description', 'genres'),
title:
anime.title?.userPreferred ||
anime.title?.english ||
anime.title?.native ||
anime.title?.romaji ||
query,
// coverImage: anime.coverImage?.large,
// bannerImage: anime.bannerImage,
tags: anime.tags?.map((tag) => tag.name),
characters: anime.characters?.map((character) => character.name),
reviews: anime.reviews
?.slice(0, 2)
.map((review) => utils.pick(review, 'summary', 'body'))
}

console.log()
console.log()
console.log('<<< search', `${query} (${openaiUserLocaleInfo}, ${ip})`)

return new Response(JSON.stringify({ anime: animeMetadata }, null, 2), {
headers: {
'content-type': 'application/json;charset=UTF-8'
}
})
}
}
24 changes: 24 additions & 0 deletions examples/anime-adventure/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface AnimeSearchResults {
pageInfo: PageInfo
media: Media[]
}

export interface Media {
id: number
title: Title
}

export interface Title {
romaji: string
english: null | string
native: string
userPreferred: string
}

export interface PageInfo {
total: number
currentPage: number
lastPage: number
hasNextPage: boolean
perPage: number
}
11 changes: 11 additions & 0 deletions examples/anime-adventure/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function pick<T extends object, U = T>(obj: T, ...keys: string[]): U {
return Object.fromEntries(
keys.filter((key) => key in obj).map((key) => [key, obj[key]])
) as U
}

export function omit<T extends object, U = T>(obj: T, ...keys: string[]): U {
return Object.fromEntries<T>(
Object.entries(obj).filter(([key]) => !keys.includes(key))
) as U
}
13 changes: 13 additions & 0 deletions examples/anime-adventure/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"composite": true,
"outDir": "build",
"tsBuildInfoFile": "build/.tsbuildinfo",
"emitDeclarationOnly": true,
"types": ["@cloudflare/workers-types"]
},
"include": ["src/**/*.ts", "package.json"],
"exclude": ["node_modules"],
"references": [{ "path": "../../packages/chatgpt-plugin/tsconfig.json" }]
}
6 changes: 6 additions & 0 deletions examples/anime-adventure/wrangler.example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name = "chatgpt-plugin-anime-adventure"
main = "src/index.ts"
compatibility_date = "2023-04-04"

[vars]
ANILIST_ACCESS_TOKEN="TODO"
29 changes: 28 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8340c46

Please sign in to comment.