From 1e45da611fdb778678c579ea60c2819cc4a66223 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Sun, 24 Mar 2024 20:12:01 +0100 Subject: [PATCH] initial commit --- .env.example | 1 + .gitignore | 178 ++++++++++++++++++++++++++++++++++++++++++ README.md | 15 ++++ context.json.example | 4 + index.ts | 74 ++++++++++++++++++ libs/Perplexity.ts | 106 +++++++++++++++++++++++++ libs/parseCommands.ts | 25 ++++++ libs/readin.ts | 11 +++ libs/required.ts | 15 ++++ package.json | 15 ++++ tsconfig.json | 27 +++++++ 11 files changed, 471 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 context.json.example create mode 100644 index.ts create mode 100644 libs/Perplexity.ts create mode 100644 libs/parseCommands.ts create mode 100644 libs/readin.ts create mode 100644 libs/required.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0ca739f --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +PPLX_API_KEY="pplx-yourkey" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28e3cc0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +bun.lockdb +config.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..982457c --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# tcsenpplx + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.0.30. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/context.json.example b/context.json.example new file mode 100644 index 0000000..a38e7ba --- /dev/null +++ b/context.json.example @@ -0,0 +1,4 @@ +[ + { "role": "user", "content": "*enters the room*" }, + { "role": "assistant", "content": "Hi! " } +] diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..c059b89 --- /dev/null +++ b/index.ts @@ -0,0 +1,74 @@ +import Perplexity from "./libs/Perplexity"; +import "dotenv/config"; +import required from "./libs/required"; +import readin from "./libs/readin"; +import fs from "fs" +import parseCommands from "./libs/parseCommands"; +import config from "./config" +import type Message from "./libs/Perplexity" +import type { ListFormat } from "typescript"; + +required(process.env.PPLX_API_KEY); +const PPLX_API_KEY = process.env.PPLX_API_KEY as string; +let perplexity: Perplexity; + +// NOTE Helper to load context from file +function inject_context() { + let loaded_context = fs.readFileSync("./context.json", {encoding: "utf-8"}) + let list_context = JSON.parse(loaded_context) + for (let i=0; i + let new_insertion = { + role: role, + content: message + } + list_context.push(new_insertion) + fs.writeFileSync("./context.json", JSON.stringify(list_context)) +} + +async function main() { + console.log("[TCSenPPLX] Working with: " + PPLX_API_KEY); + // Connecting to perplexity + perplexity = new Perplexity( + PPLX_API_KEY, + "https://api.perplexity.ai", + "sonar-medium-chat", + false, + {} + ); + // Setting our ai personality + perplexity.add_to_context(config); + // If any, inject context + inject_context() + // Chatting + await chat(); +} + +async function chat(loop: boolean = true, save: boolean = true) { + var proceed = true; + while (proceed) { + proceed = loop; + let question = await readin("You: "); + let parsed = parseCommands(question); + proceed = parsed.proceed + let logpart = parsed.logpart + let response = await perplexity.ask(question); + console.log("Assistant: " + response); + if (save) { + save_in_context("user", question) + if (logpart) { + response += +"\n[" + logpart + "]" + } + save_in_context("assistant", response as string) + } + } +} + +main(); diff --git a/libs/Perplexity.ts b/libs/Perplexity.ts new file mode 100644 index 0000000..14b457a --- /dev/null +++ b/libs/Perplexity.ts @@ -0,0 +1,106 @@ +// Wrapper class for easy tasking with Perplexity (and OpenAI in general) + +import OpenAI from "openai"; +import type { RequestOptions } from "openai/core.mjs"; + +interface Message { + role: string; + content: string; +} + +interface Context { + messages: Message[]; +} + +export default class Perplexity { + API_KEY: string; + instance: OpenAI; + current_model: string; + verbose: boolean; + + // Instance vars + roles = { + a: "system", + b: "user", + c: "assistant", + }; + context: Context = { messages: [] }; + options: RequestOptions = {} + + constructor( + api_key: string, + base_url: string = "https://api.perplexity.ai", + base_model: string = "sonar-small-chat", + verbose: boolean = false, + options: RequestOptions = {} + ) { + // Setting our API Key + this.API_KEY = api_key; + this.current_model = base_model; + // Setting verbosity + this.verbose = verbose; + // Setting options + this.options = options; + // Creating an openai agent + this.instance = new OpenAI({ + baseURL: base_url, + apiKey: this.API_KEY, // This is the default and can be omitted + }); + } + + // INFO Adding something to context + add_to_context(message: Message) { + // Validate roles + if ( + message.role != this.roles.a && + message.role != this.roles.b && + message.role != this.roles.c + ) { + console.log("[ERROR] Not added. Possible roles can be:"); + console.log(this.roles); + return false; + } + this.context.messages.push(message); + if(this.verbose) console.log("[OK] Added to context for: " + message.role); + } + + // INFO Setting a default model + set_current_model(model: string) { + this.current_model = model; + } + + // INFO Context viewer + show_context() { + console.log(this.context) + } + + // INFO Wrapper for asking + async ask(question: string) { + let insertion = { + role: "user", + content: question, + }; + this.add_to_context(insertion); + let response = await this.complete(false, this.options); + this.add_to_context(response as Message); + return response.content; + } + + // INFO Wrapper around completion + async complete(override_model?: string | boolean, options: RequestOptions = {}) { + if (this.verbose) console.log("[WORKING] Thinking..."); + let used_model = override_model ? override_model : this.current_model; + const chatCompletion = await this.instance.chat.completions.create( + { + messages: this.context.messages as [], + model: used_model as string, + stream: false, + }, + options + ); + let return_prompt = chatCompletion.choices[0].message; + return return_prompt; + } + + // TODO Support streams if needed +} diff --git a/libs/parseCommands.ts b/libs/parseCommands.ts new file mode 100644 index 0000000..fecee5e --- /dev/null +++ b/libs/parseCommands.ts @@ -0,0 +1,25 @@ +export interface ContextPart { + role: string + content: string +} + +export default function parseCommands(input: string): {proceed: boolean, logpart: string | null} { + let proceed = true + let logpart = null + switch (input) { + case "exit": + case "quit": + case "stop": + case "end": + case "bye": + proceed = false + logpart = "Last conversation ended at: " + String(Date.now()) + break + default: + break + } + return { + proceed: proceed, + logpart: logpart + } +} \ No newline at end of file diff --git a/libs/readin.ts b/libs/readin.ts new file mode 100644 index 0000000..926b7b9 --- /dev/null +++ b/libs/readin.ts @@ -0,0 +1,11 @@ +import * as readline from "readline/promises"; + +export default async function readin(q: string = ""): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + let answer = await rl.question(q); + rl.close(); + return answer; +} diff --git a/libs/required.ts b/libs/required.ts new file mode 100644 index 0000000..40e9b2e --- /dev/null +++ b/libs/required.ts @@ -0,0 +1,15 @@ +export default function required( + condition: any, + message: string = "Oops! Something went wrong!", + fatal: boolean = true, + exitcode: number = -1 +) { + if (condition) return + + if (fatal) { + console.log("[FATAL] " + message); + process.exit(exitcode); + } else { + console.log("[ERROR] " + message); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..99b40ca --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "tcsenpplx", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "dotenv": "^16.4.5", + "openai": "^4.29.2" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0fef23a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}