From 62c9507cd4ec38d35cc87929998d7125dea6b938 Mon Sep 17 00:00:00 2001 From: Dani Date: Fri, 26 Jul 2024 05:14:29 -0400 Subject: [PATCH 1/4] worst typescript ever written by yours truly --- apps/api/.gitignore | 175 ++++++++++++++++++++++++++++ apps/api/README.md | 15 +++ apps/api/package.json | 11 ++ apps/api/src/index.ts | 23 ++++ apps/api/src/routes/oai/index.ts | 24 ++++ apps/api/src/types/global.ts | 18 +++ apps/api/tsconfig.json | 27 +++++ apps/core/.gitignore | 175 ++++++++++++++++++++++++++++ apps/core/README.md | 15 +++ apps/core/package.json | 11 ++ apps/core/src/index.ts | 22 ++++ apps/core/src/lib/LangChain.ts | 119 +++++++++++++++++++ apps/core/src/lib/globals.ts | 86 ++++++++++++++ apps/core/src/routes/chat/create.ts | 9 ++ apps/core/src/routes/ws.ts | 23 ++++ apps/core/src/types/ws.ts | 48 ++++++++ apps/core/tsconfig.json | 27 +++++ bun.lockb | Bin 0 -> 43561 bytes index.ts | 1 + package.json | 40 +++++++ tsconfig.json | 27 +++++ 21 files changed, 896 insertions(+) create mode 100644 apps/api/.gitignore create mode 100644 apps/api/README.md create mode 100644 apps/api/package.json create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/routes/oai/index.ts create mode 100644 apps/api/src/types/global.ts create mode 100644 apps/api/tsconfig.json create mode 100644 apps/core/.gitignore create mode 100644 apps/core/README.md create mode 100644 apps/core/package.json create mode 100644 apps/core/src/index.ts create mode 100644 apps/core/src/lib/LangChain.ts create mode 100644 apps/core/src/lib/globals.ts create mode 100644 apps/core/src/routes/chat/create.ts create mode 100644 apps/core/src/routes/ws.ts create mode 100644 apps/core/src/types/ws.ts create mode 100644 apps/core/tsconfig.json create mode 100644 bun.lockb create mode 100644 index.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1,175 @@ +# 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 diff --git a/apps/api/README.md b/apps/api/README.md new file mode 100644 index 0000000..463990f --- /dev/null +++ b/apps/api/README.md @@ -0,0 +1,15 @@ +# @pathwaysml/core + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run src/index.ts +``` + +This project was created using `bun init` in bun v1.1.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..e2155d0 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,11 @@ +{ + "name": "@pathwaysml/api", + "module": "src/index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..130c370 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,23 @@ +import { Elysia, t } from 'elysia' +import { autoroutes } from 'elysia-autoroutes' + +const app = new Elysia() + .use( + autoroutes({ + routesDir: "./routes", // -> optional, defaults to './routes' + generateTags: false, // -> optional, defaults to true + }) + ) + .onRequest(({ set }) => { + // change Powered-By header + set.headers['x-powered-by'] = "@pathwaysml/api: API compatibility layers for Pathways Engine"; + }) + .guard({ + response: t.Object({ + status: t.Integer(), + response: t.Any() + }) + }) + .listen(Bun.env.PWSE_API_PORT ?? 3000) + +export type ElysiaApp = typeof app \ No newline at end of file diff --git a/apps/api/src/routes/oai/index.ts b/apps/api/src/routes/oai/index.ts new file mode 100644 index 0000000..01d5077 --- /dev/null +++ b/apps/api/src/routes/oai/index.ts @@ -0,0 +1,24 @@ +import type { ElysiaApp } from "../../index.ts"; +import { SupportedCalls, type APIMetadataResponse, type APIVersion } from "../../types/global.ts"; + +export const Metadata: APIVersion = { + identifier: "v1", + name: "OpenAI-Compatible API", + supports: [ + SupportedCalls.TraditionalInference, + SupportedCalls.StreamingInference, + SupportedCalls.TextGeneration, + SupportedCalls.ImageGeneration, + SupportedCalls.Integrations, + ] +} + +const Route = (app: ElysiaApp) => app.get("/", (): APIMetadataResponse => { + return { + status: 200, + response: Metadata + } +}); + + +export default Route; \ No newline at end of file diff --git a/apps/api/src/types/global.ts b/apps/api/src/types/global.ts new file mode 100644 index 0000000..0a405c3 --- /dev/null +++ b/apps/api/src/types/global.ts @@ -0,0 +1,18 @@ +export enum SupportedCalls { + TraditionalInference = "traditionalInference", + StreamingInference = "streamingInference", + TextGeneration = "textGeneration", + ImageGeneration = "imageGeneration", + Integrations = "integrations", +} + +export type APIVersion = { + identifier: string, + name: string, + supports: SupportedCalls[], +} + +export type APIMetadataResponse = { + status: number, + response: APIVersion | string, +} \ No newline at end of file diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "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 + } +} diff --git a/apps/core/.gitignore b/apps/core/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/apps/core/.gitignore @@ -0,0 +1,175 @@ +# 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 diff --git a/apps/core/README.md b/apps/core/README.md new file mode 100644 index 0000000..463990f --- /dev/null +++ b/apps/core/README.md @@ -0,0 +1,15 @@ +# @pathwaysml/core + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run src/index.ts +``` + +This project was created using `bun init` in bun v1.1.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/apps/core/package.json b/apps/core/package.json new file mode 100644 index 0000000..065e925 --- /dev/null +++ b/apps/core/package.json @@ -0,0 +1,11 @@ +{ + "name": "@pathwaysml/core", + "module": "src/index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/apps/core/src/index.ts b/apps/core/src/index.ts new file mode 100644 index 0000000..1797329 --- /dev/null +++ b/apps/core/src/index.ts @@ -0,0 +1,22 @@ +import { Elysia } from 'elysia' +import { autoroutes } from 'elysia-autoroutes' +import { IORedis } from './lib/globals'; + +await IORedis; + +const app = new Elysia() + .use( + autoroutes({ + routesDir: "./routes", // -> optional, defaults to './routes' + generateTags: false, // -> optional, defaults to true + }) + ) + .onRequest(({ set }) => { + // change Powered-By header + set.headers['x-powered-by'] = "@pathwaysml/core: Pathways Engine"; + }) + .listen(Bun.env.PWSE_CORE_PORT ?? 3001); + +type BaseElysiaApp = typeof app; + +export interface ElysiaApp extends BaseElysiaApp { }; \ No newline at end of file diff --git a/apps/core/src/lib/LangChain.ts b/apps/core/src/lib/LangChain.ts new file mode 100644 index 0000000..0256b34 --- /dev/null +++ b/apps/core/src/lib/LangChain.ts @@ -0,0 +1,119 @@ +import type { ChatOpenAI } from "@langchain/openai"; +import type { ElysiaApp } from "../index.ts"; +import { providers } from "./globals.ts"; +import type { ChatOllama } from "@langchain/ollama"; +import { RedisByteStore } from "@langchain/community/storage/ioredis"; +import type { Redis } from "ioredis"; + +export interface ChatOptions { + provider: string; + model: string; + conversationId: string; +} + +export type ChatRole = "user" | "assistant" | "system" | "tool"; + +export interface HistoryMessage { + role: "user" | "assistant" | "system" | "tool"; + id: string; + timestamp: string; + content: string; + user: { + id: string; + name: string; + displayName: string | null; + pronouns: string; + } +} + +export class History { + protected _redis: Redis; + protected _store: RedisByteStore; + protected _encoder: TextEncoder; + protected _decoder: TextDecoder; + conversationId: string; + + constructor(redis: Redis, conversationId: string) { + this._redis = redis; + this.conversationId = conversationId; + this._encoder = new TextEncoder(); + this._decoder = new TextDecoder(); + this._store = new RedisByteStore({ + client: this._redis + }); + } + + async get(k: string | string[]): Promise { + const toQuery = [k].flat().map((v) => `${this.conversationId}:${v}`); + if (toQuery.length === 0) return []; + + const query = await this._store.mget(toQuery || []); + + return query.map((v) => { + const _d: HistoryMessage = JSON.parse(this._decoder.decode(v)) || {}; + const role: ChatRole = _d.role || "user"; + + return { + role, + id: _d.id || "0", + timestamp: _d.timestamp || new Date().toISOString(), + content: _d.content || "[empty]", + user: { + id: _d.user?.id || "0", + name: _d.user?.name || "unknown", + displayName: _d.user?.displayName ?? _d.user?.name, + pronouns: _d.user?.pronouns || "unknown", + } + } + }); + } + + async getAll(): Promise { + const keyGenerator = this._store.yieldKeys(`${this.conversationId}:`); + + const keys: string[] = []; + for await (const key of keyGenerator) { + keys.push(key); + } + + return await this.get(keys || []); + } + + async add(data: HistoryMessage | HistoryMessage[]) { + if (Array.isArray(data)) { + const query = await this._store.mset(data.map((v) => [ + `${this.conversationId}:${v.id}`, + this._encoder.encode(JSON.stringify(v)) + ])) + + return query; + } else { + const query = await this._store.mset([ + [ + `${this.conversationId}:${data.id}`, + this._encoder.encode(JSON.stringify(data)) + ] + ]) + + return query; + } + } +} + +export class Chat { + protected _provider: string; + protected _model: string; + conversationId: string; + langChainAccessor: ChatOpenAI | ChatOllama; + + constructor({ provider, model, conversationId }: ChatOptions) { + this._provider = process.env.LANGCHAIN_PROVIDER ?? "openai"; + this._model = process.env.LANGCHAIN_MODEL ?? "gpt-4o-mini"; + this.conversationId = conversationId; + this.langChainAccessor = providers[this._provider].generator(this._model); + } + + async singleCall({ message, attachments }: { message: string, attachments: any[] }) { + const chatCompletion = await this.langChainAccessor; + } +} \ No newline at end of file diff --git a/apps/core/src/lib/globals.ts b/apps/core/src/lib/globals.ts new file mode 100644 index 0000000..606f4b3 --- /dev/null +++ b/apps/core/src/lib/globals.ts @@ -0,0 +1,86 @@ +import { ChatOllama } from "@langchain/ollama" +import { ChatOpenAI } from "@langchain/openai" +import { Redis } from "ioredis"; +import { nanoid } from "nanoid"; +import chalk from "chalk"; +import type { Chat } from "./LangChain"; + +interface Provider { + generator: (model: string) => ChatOpenAI | ChatOllama; +} + +export const providers: { [key: string]: Provider } = { + "openai": { + generator: (model: string) => { + const _m = new ChatOpenAI({ + model, + apiKey: process.env.OPENAI_API_KEY, + configuration: { + baseURL: process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1", + } + }) + + return _m + } + }, + "openrouter": { + generator: (model: string) => { + const _m = new ChatOpenAI({ + model, + apiKey: process.env.OPENROUTER_API_KEY, + configuration: { + baseURL: process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/api/v1", + } + }) + + return _m + } + }, + "ollama": { + generator: (model: string) => { + const _m = new ChatOllama({ + model, + baseUrl: process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434", + }) + + return _m + } + }, +} + +// redis://192.168.5.2:6379 +const decodeRedisURI = (uri: string) => { + const url = new URL(uri); + return { + host: url.hostname, + port: parseInt(url.port), + password: url.password, + }; +}; + +const generateRedisClient = async () => { + const decodedURI = decodeRedisURI(process.env.KV_URI ?? "redis://localhost:6379"); + + const IORedis = new Redis({ + host: decodedURI.host, + port: decodedURI.port, + password: decodedURI.password, + db: parseInt(process.env.KV_CHANNEL ?? "0"), + connectionName: `pwse.core: ${nanoid()}` + }); + + IORedis.on("connect", () => { + console.log(chalk.green("Redis connected!")) + }); + + IORedis.on("error", (err) => { + console.error(chalk.red("Redis error!")); + console.error(err); + }); + + await IORedis.connect().catch(() => { }); + + return IORedis; +} + +export const IORedis = generateRedisClient(); \ No newline at end of file diff --git a/apps/core/src/routes/chat/create.ts b/apps/core/src/routes/chat/create.ts new file mode 100644 index 0000000..d570c96 --- /dev/null +++ b/apps/core/src/routes/chat/create.ts @@ -0,0 +1,9 @@ + +import type { ElysiaApp } from "../../index.ts"; + +const Route = (app: ElysiaApp) => app.get('/', function () { + return {}; +}) + + +export default Route; \ No newline at end of file diff --git a/apps/core/src/routes/ws.ts b/apps/core/src/routes/ws.ts new file mode 100644 index 0000000..e290040 --- /dev/null +++ b/apps/core/src/routes/ws.ts @@ -0,0 +1,23 @@ +import { t } from "elysia"; +import type { ElysiaApp } from "../index.ts"; +import Types from "../types/ws.ts"; + +export const Metadata = { + identifier: "v1", + name: "OpenAI-Compatible API", +} + +const Route = (app: ElysiaApp) => app.ws("/", { + body: t.Union(Types), + message: async (ws, { procedure, data }): Promise => { + if (procedure === "helloWorld") { + ws.send({ + status: 200, + response: "Hello World!" + }) + } + } +}); + + +export default Route; \ No newline at end of file diff --git a/apps/core/src/types/ws.ts b/apps/core/src/types/ws.ts new file mode 100644 index 0000000..6a51d7d --- /dev/null +++ b/apps/core/src/types/ws.ts @@ -0,0 +1,48 @@ +import { t } from "elysia"; + +const event = { + shared: t.Object({ + type: t.Literal("event"), + procedure: t.Union([ + t.Literal("subscribe"), + t.Literal("unsubscribe"), + t.Literal("metadata"), + ]), + data: t.Object({ + id: t.String(), + }) + }) +} + +const system = { + helloWorld: t.Object({ + type: t.Literal("system"), + procedure: t.Literal("helloWorld"), + data: t.Object({ + foo: t.String(), + }) + }) +} + +const chat = { + message: t.Object({ + type: t.Literal("chat"), + procedure: t.Literal("message"), + data: t.Object({ + id: t.String(), + user: t.Object({ + id: t.String(), + name: t.String(), + pronouns: t.String(), + }), + content: t.String(), + }) + }) +} + + +export default [ + ...Object.values(event), + ...Object.values(system), + ...Object.values(chat), +] \ No newline at end of file diff --git a/apps/core/tsconfig.json b/apps/core/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/apps/core/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "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 + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..9de3d7fad8447a0657fe568a2688e4c4b318728e GIT binary patch literal 43561 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 literal 0 HcmV?d00001 diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..661c77b --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "@pathwaysml/engine", + "author": { + "name": "Pathways", + "url": "https://pathways.spongeass.dev" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "contributors": [ + { + "name": "SpongeAss", + "url": "https://github.com/spongedsc" + } + ], + "scripts": { + "run:api": "bun run apps/api/src/index.ts", + "run:core": "bun run apps/core/src/index.ts" + }, + "type": "module", + "workspaces": [ + "packages/*", + "apps/*" + ], + "dependencies": { + "@langchain/community": "^0.2.20", + "@langchain/core": "^0.2.18", + "@langchain/ollama": "^0.0.2", + "@langchain/openai": "^0.2.5", + "chalk": "^5.3.0", + "elysia": "^1.1.4", + "elysia-autoroutes": "^0.5.0", + "ioredis": "^5.4.1", + "langchain": "^0.2.11", + "nanoid": "^5.0.7" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "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 + } +} From 04c40c4cae0f4cb072338215b8d622f7c2ba00d7 Mon Sep 17 00:00:00 2001 From: Dani Date: Tue, 30 Jul 2024 03:16:24 -0400 Subject: [PATCH 2/4] insane amounts of spaghetti code --- .gitignore | 3 + apps/core/src/index.ts | 8 +- apps/core/src/lib/Integrations.ts | 163 +++++++++++++++ apps/core/src/lib/LangChain.ts | 181 ++++++++++++++-- apps/core/src/lib/globals.ts | 51 ++++- apps/core/src/routes/chat/create.ts | 55 ++++- bun.lockb | Bin 43561 -> 53015 bytes package.json | 7 +- packages/pwse-weather/.gitignore | 175 ++++++++++++++++ packages/pwse-weather/README.md | 15 ++ packages/pwse-weather/index.js | 129 ++++++++++++ packages/pwse-weather/localisation.js | 28 +++ packages/pwse-weather/package.json | 14 ++ packages/pwse-weather/tsconfig.json | 27 +++ packages/pwse-weather/wmo.data.js | 283 ++++++++++++++++++++++++++ 15 files changed, 1113 insertions(+), 26 deletions(-) create mode 100644 apps/core/src/lib/Integrations.ts create mode 100644 packages/pwse-weather/.gitignore create mode 100644 packages/pwse-weather/README.md create mode 100644 packages/pwse-weather/index.js create mode 100644 packages/pwse-weather/localisation.js create mode 100644 packages/pwse-weather/package.json create mode 100644 packages/pwse-weather/tsconfig.json create mode 100644 packages/pwse-weather/wmo.data.js diff --git a/.gitignore b/.gitignore index c6bba59..293792d 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# PWSE configuration +pwse.config.* diff --git a/apps/core/src/index.ts b/apps/core/src/index.ts index 1797329..7c5df87 100644 --- a/apps/core/src/index.ts +++ b/apps/core/src/index.ts @@ -1,10 +1,16 @@ import { Elysia } from 'elysia' import { autoroutes } from 'elysia-autoroutes' import { IORedis } from './lib/globals'; +import { logger } from "@bogeychan/elysia-logger"; -await IORedis; +await IORedis.connect().catch(() => { }); const app = new Elysia() + .use( + logger({ + level: "error", + }) + ) .use( autoroutes({ routesDir: "./routes", // -> optional, defaults to './routes' diff --git a/apps/core/src/lib/Integrations.ts b/apps/core/src/lib/Integrations.ts new file mode 100644 index 0000000..3a8abee --- /dev/null +++ b/apps/core/src/lib/Integrations.ts @@ -0,0 +1,163 @@ +import type { Redis } from "ioredis" +import type { Chat, ChatMessage, HistoryMessage } from "./LangChain" +import { config, IORedis, providers } from "./globals" +import z from "zod"; +import { AIMessageChunk } from "@langchain/core/messages"; +import { convertToOpenAITool } from "@langchain/core/utils/function_calling"; +import { nanoid } from "nanoid"; +import zodToJsonSchema from "zod-to-json-schema"; +import type { ChatOllama } from "@langchain/ollama"; +import type { ChatOpenAI } from "@langchain/openai"; + +export interface IntegrationRequest { + name: string; + id: string; + type: "tool_call" | "function"; + args?: Record; + responseMetadata?: Record; +} + +export interface IntegrationTask { + name: string; + description: string; + args: Record; +} + +export enum IntegrationStatus { + Submitted = "submitted", + Queued = "queued", + Pending = "pending", + Completed = "completed", + Failed = "failed", + FatalError = "fatal", +} + +export interface IntegrationResponse { + status: IntegrationStatus; + content: string; + attachments?: any[]; + metadata?: Record; + timestamp: string; + integration: { + id: string; + name: string; + description: string; + arguments: Record; + passedArguments?: Record; + } +} + +export const IntegrationArgument = z.object({ + name: z.string(), + description: z.string(), + required: z.boolean(), +}) + +export const Integration = z.object({ + id: z.string(), + name: z.string(), + description: z.string(), + arguments: z.record(IntegrationArgument), + execute: z.any(), +}) + + +export class IntegrationsRunner { + protected _redis: Redis + protected _chat: Chat + protected _langChainAccessor: ChatOpenAI | ChatOllama; + + constructor(redis: Redis, chat: Chat, callerProvider: string, callerModel: string) { + this._redis = redis || IORedis; + this._chat = chat; + this._langChainAccessor = providers[callerProvider].generator(callerModel); + }; + + async run(integrations: IntegrationRequest[]) { + const responses: IntegrationResponse[] = []; + for await (const integration of integrations) { + const task: IntegrationResponse = await this.invokeCall(integration); + responses.push(task); + } + + return responses; + } + + async invokeCall(integrationRequest: IntegrationRequest) { + const { name, args } = integrationRequest; + + // get all integrations from config + const allIntegrations = config.integrations; + + // find the relevant integration + const integration = allIntegrations.find((i) => i.name === name); + + // if the integration exists, execute it + if (integration?.func) { + const task: IntegrationResponse = await integration.func(args); + return { + ...task, + integration: { + id: integrationRequest.id, + name: integration.name, + description: integration.description, + arguments: zodToJsonSchema(integration.schema), + passedArguments: args + } + }; + } else { + return { + status: IntegrationStatus.FatalError, + content: "Integration not found", + metadata: { + name, + args, + }, + timestamp: new Date().toISOString(), + integration: { + id: integrationRequest.id, + name: integration?.name ?? "unknown", + description: integration?.description ?? "unknown", + arguments: integration !== undefined ? zodToJsonSchema(integration.schema) : {}, + passedArguments: args + } + } + }; + } + + async invoke(messages: HistoryMessage[]): Promise<{ chatCompletion: AIMessageChunk, tasks: IntegrationRequest[] }> { + const ctx: ChatMessage[] = this._chat.history.transform(messages); + const ifm = this._langChainAccessor.bind({ + tools: config.integrations.map(convertToOpenAITool), + parallel_tool_calls: false + }); + + const out = await ifm.invoke(ctx).catch((err) => { + console.error(err); + return new AIMessageChunk({ + content: "An error occurred while processing your request. Please try again later.", + }) + }); + + // remove duplicates + const uniqueIntegrations = out.tool_calls?.filter((v, i, a) => a.findIndex(t => t.name === v.name) === i) || []; + + const integrationsCalled = uniqueIntegrations.map((v) => { + const integration = config.integrations.find((i) => i.name === v.name); + if (!integration) return null; + + return { + name: integration.name, + id: v.id ?? nanoid(), + type: v.type ?? "tool_call", + args: v.args, + responseMetadata: {} + } + }).filter((v) => v !== null); + + return { + tasks: integrationsCalled, + chatCompletion: out + } + } +} \ No newline at end of file diff --git a/apps/core/src/lib/LangChain.ts b/apps/core/src/lib/LangChain.ts index 0256b34..debbcf4 100644 --- a/apps/core/src/lib/LangChain.ts +++ b/apps/core/src/lib/LangChain.ts @@ -1,31 +1,54 @@ import type { ChatOpenAI } from "@langchain/openai"; -import type { ElysiaApp } from "../index.ts"; -import { providers } from "./globals.ts"; +import { config, IORedis, providers } from "./globals.ts"; import type { ChatOllama } from "@langchain/ollama"; import { RedisByteStore } from "@langchain/community/storage/ioredis"; import type { Redis } from "ioredis"; +import { HumanMessage, AIMessage, FunctionMessage, RemoveMessage, SystemMessage, ToolMessage, AIMessageChunk } from "@langchain/core/messages"; +import { nanoid } from "nanoid"; +import { IntegrationsRunner } from "./Integrations.ts"; +import type { IntegrationRequest } from "./Integrations"; export interface ChatOptions { provider: string; model: string; + callerProvider: string; + callerModel: string; conversationId: string; } -export type ChatRole = "user" | "assistant" | "system" | "tool"; +export enum ChatRole { + User = "user", + Assistant = "assistant", + System = "system", + Tool = "tool", +} export interface HistoryMessage { - role: "user" | "assistant" | "system" | "tool"; + role: ChatRole; id: string; timestamp: string; content: string; - user: { + user?: { id: string; name: string; - displayName: string | null; + displayName: string | null | undefined; pronouns: string; + }, + tools?: IntegrationRequest[]; + toolCalled?: { + id: string; + name: string; + args?: Record; } } +export type ChatMessage = SystemMessage | HumanMessage | AIMessage | ToolMessage | FunctionMessage | RemoveMessage; + +export interface SendChatMessageOptions { + messages: HistoryMessage | HistoryMessage[]; + attachments?: any[]; +} + export class History { protected _redis: Redis; protected _store: RedisByteStore; @@ -51,7 +74,15 @@ export class History { return query.map((v) => { const _d: HistoryMessage = JSON.parse(this._decoder.decode(v)) || {}; - const role: ChatRole = _d.role || "user"; + + const enumMappings = { + "user": ChatRole.User, + "assistant": ChatRole.Assistant, + "system": ChatRole.System, + "tool": ChatRole.Tool, + } + + const role: ChatRole = enumMappings[_d.role] ?? ChatRole.User; return { role, @@ -59,13 +90,13 @@ export class History { timestamp: _d.timestamp || new Date().toISOString(), content: _d.content || "[empty]", user: { - id: _d.user?.id || "0", - name: _d.user?.name || "unknown", + id: _d.user?.id ?? "0", + name: _d.user?.name ?? "unknown", displayName: _d.user?.displayName ?? _d.user?.name, - pronouns: _d.user?.pronouns || "unknown", + pronouns: _d.user?.pronouns ?? "unknown", } } - }); + }).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); } async getAll(): Promise { @@ -73,12 +104,23 @@ export class History { const keys: string[] = []; for await (const key of keyGenerator) { - keys.push(key); + keys.push(key.replace(`${this.conversationId}:`, "")); } return await this.get(keys || []); } + async allKeys(): Promise { + const keyGenerator = this._store.yieldKeys(`${this.conversationId}:`); + + const keys: string[] = []; + for await (const key of keyGenerator) { + keys.push(key.replace(`${this.conversationId}:`, "")); + } + + return keys; + } + async add(data: HistoryMessage | HistoryMessage[]) { if (Array.isArray(data)) { const query = await this._store.mset(data.map((v) => [ @@ -98,22 +140,127 @@ export class History { return query; } } + + transform(data: HistoryMessage[]) { + return data.map((v) => { + if (v.role === "user") { + return new HumanMessage(v.content); + } else if (v.role === "system") { + return new SystemMessage(v.content); + } else if (v.role === "tool") { + return new ToolMessage({ + tool_call_id: v.id, + content: v.content, + name: v.toolCalled?.name, + additional_kwargs: v.toolCalled?.args + }); + } else { + return new AIMessage({ + content: v.content, + tool_calls: v.tools as any + }); + } + }); + }; } export class Chat { protected _provider: string; protected _model: string; + protected _callerProvider: string; + protected _callerModel: string; conversationId: string; langChainAccessor: ChatOpenAI | ChatOllama; + history: History; + integrations: IntegrationsRunner; - constructor({ provider, model, conversationId }: ChatOptions) { - this._provider = process.env.LANGCHAIN_PROVIDER ?? "openai"; - this._model = process.env.LANGCHAIN_MODEL ?? "gpt-4o-mini"; + constructor({ provider, model, callerProvider, callerModel, conversationId }: ChatOptions) { + this._provider = provider ?? process.env.LANGCHAIN_PROVIDER ?? "openai"; + this._model = model ?? process.env.LANGCHAIN_MODEL ?? "gpt-4o-mini"; + this._callerProvider = callerProvider ?? process.env.LANGCHAIN_CALLER_PROVIDER ?? "openai"; + this._callerModel = callerModel ?? process.env.LANGCHAIN_CALLER_MODEL ?? "gpt-4o-mini"; this.conversationId = conversationId; this.langChainAccessor = providers[this._provider].generator(this._model); + this.history = new History(IORedis, conversationId); + this.integrations = new IntegrationsRunner(IORedis, this, this._callerProvider, this._callerModel); + } + + private async _invoke(messages: HistoryMessage[]): Promise { + const ctx: ChatMessage[] = this.history.transform(messages); + const lastMsg: HistoryMessage = messages[messages.length - 1]; + + const chatCompletion: AIMessageChunk = await this.langChainAccessor.bind({ + tools: config.integrations as any + }).invoke(ctx); + + await this.history.add([ + { + id: (messages?.[messages.length - 1]?.id ? messages[messages.length - 1].id + "ASSISTANT" : `UNKNOWN:${nanoid()}`), + timestamp: new Date().toISOString(), + role: ChatRole.Assistant, + content: chatCompletion.content as string, + user: { + id: lastMsg?.user?.id ?? `UNKNOWN:${nanoid()}`, + name: lastMsg?.user?.name ?? "Unknown", + displayName: lastMsg?.user?.displayName ?? lastMsg?.user?.name, + pronouns: lastMsg?.user?.pronouns ?? "Unknown", + } + } + ]); + + return chatCompletion; } - async singleCall({ message, attachments }: { message: string, attachments: any[] }) { - const chatCompletion = await this.langChainAccessor; + async send({ messages, attachments }: SendChatMessageOptions) { + const history = await this.history.getAll(); + + const untransformedCtx: HistoryMessage[] = [ + ...history, + ...(Array.isArray(messages) ? messages : [messages]), + ]; + + await this.history.add(messages); + + const { tasks: toCall, chatCompletion } = await this.integrations.invoke(untransformedCtx); + if (toCall?.length) { + const tasks = await this.integrations.run(toCall); + const taskMessages = tasks.map((v) => { + return { + role: ChatRole.Tool, + id: v.integration.id, + timestamp: v.timestamp, + content: v.content, + toolCalled: { + id: v.integration.id, + name: v.integration.name, + args: v.integration.passedArguments + } + }; + }); + + const completedCtx = await this._invoke([ + ...untransformedCtx, + { + role: ChatRole.Assistant, + id: nanoid(), + timestamp: new Date().toISOString(), + content: chatCompletion.content as string, + tools: tasks.map((v) => ({ + id: v.integration.id, + type: "tool_call", + name: v.integration.name, + args: v.integration.passedArguments, + })) + }, + ...taskMessages + ]); + + return { + ...completedCtx, + taskResults: tasks + }; + } else { + return chatCompletion; + } } } \ No newline at end of file diff --git a/apps/core/src/lib/globals.ts b/apps/core/src/lib/globals.ts index 606f4b3..be64107 100644 --- a/apps/core/src/lib/globals.ts +++ b/apps/core/src/lib/globals.ts @@ -3,7 +3,14 @@ import { ChatOpenAI } from "@langchain/openai" import { Redis } from "ioredis"; import { nanoid } from "nanoid"; import chalk from "chalk"; -import type { Chat } from "./LangChain"; +import path from "node:path"; +import dedent from "dedent"; +import type { DynamicStructuredTool } from "@langchain/core/tools"; + +export interface Configuration { + integrations: DynamicStructuredTool[]; + plugins?: any[]; +} interface Provider { generator: (model: string) => ChatOpenAI | ChatOllama; @@ -70,12 +77,12 @@ const generateRedisClient = async () => { }); IORedis.on("connect", () => { - console.log(chalk.green("Redis connected!")) + console.log(chalk.bold(`${chalk.green("✔")} Connected to IORedis (History client)`)) }); IORedis.on("error", (err) => { - console.error(chalk.red("Redis error!")); - console.error(err); + console.error(chalk.bold(`${chalk.red("✖")} An error occurred while connecting to Redis`)); + throw err; }); await IORedis.connect().catch(() => { }); @@ -83,4 +90,38 @@ const generateRedisClient = async () => { return IORedis; } -export const IORedis = generateRedisClient(); \ No newline at end of file +export const generateGlobalConfig = async () => { + // get import dir of this file + const currentDir = path.resolve(import.meta.dirname); + + // config dir is three four levels up + const configDir = path.join(currentDir, "..", "..", "..", ".."); + + // load pwse.config.ts first; if not found, load pwse.config.js + const configFile = path.join(configDir, "pwse.config.ts"); + + // attempt import of .ts first + try { + const configModule = await import(configFile); + console.log(chalk.bold(`${chalk.green("✔")} Loaded ${chalk.blue("pwse.config.ts")}`)); + return configModule.default as Configuration; + } catch (err) { + // if import fails, attempt import of .js + try { + const configModule = await import(configFile.replace(".ts", ".js")); + console.log(chalk.bold(`${chalk.green("✔")} Loaded ${chalk.yellow("pwse.config.js")}`)); + return configModule.default as Configuration; + } catch (err) { + // if import fails, throw error + throw new Error(dedent`An error occurred during the configuration load. + No configuration file was found in the apex directory, ${chalk.blue(configDir)}. + Please create one of the following files in this directory: + ${chalk.blue("pwse.config.ts")} in TypeScript + ${chalk.yellow("pwse.config.js")} in JavaScript + `); + } + } +} + +export const IORedis = await generateRedisClient(); +export const config = await generateGlobalConfig(); \ No newline at end of file diff --git a/apps/core/src/routes/chat/create.ts b/apps/core/src/routes/chat/create.ts index d570c96..44dcc92 100644 --- a/apps/core/src/routes/chat/create.ts +++ b/apps/core/src/routes/chat/create.ts @@ -1,8 +1,59 @@ +import { nanoid } from "nanoid"; import type { ElysiaApp } from "../../index.ts"; +import { Chat, ChatRole } from "../../lib/LangChain.ts"; -const Route = (app: ElysiaApp) => app.get('/', function () { - return {}; +const Route = (app: ElysiaApp) => app.get('/', async (req) => { + if (!req.query.q) { + return { + status: 400, + body: { + error: "Missing query parameter 'q'" + } + } + } else if (!req.query.conversation) { + return { + status: 400, + body: { + error: "Missing query parameter 'conversation'" + } + } + } + const chat = new Chat({ + provider: "openai", + model: "gpt-4o-mini", + callerProvider: "ollama", + callerModel: "llama3.1", + conversationId: req.query.conversation.trim() ?? "test" + }); + + const request: any = await chat.send({ + messages: [ + { + id: nanoid(), + timestamp: new Date().toISOString(), + role: ChatRole.User, + content: req.query.q?.trim() ?? "Create an error message and tell the user to try using a proper query parameter", + user: { + id: nanoid(), + name: "User", + displayName: "User", + pronouns: "he/him" + } + } + ] + }); + + return { + content: request.content, + metadata: request.response_metadata, + tasks: request.taskResults, + usage: { + input: request.usage_metadata?.input_tokens, + output: request.usage_metadata?.output_tokens, + total: request.usage_metadata?.total_tokens + } + } }) diff --git a/bun.lockb b/bun.lockb index 9de3d7fad8447a0657fe568a2688e4c4b318728e..8f47f4182a8f3328c415ca4fe268c3b774d415ca 100644 GIT binary patch delta 14766 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 W5ISa@Dl9O9g{){%_3`F%{{I2XS^S*< delta 8853 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 + new DynamicStructuredTool({ + name: "current_weather", + description: "Get the weather for a given location", + schema: z.object({ + location: z + .string() + .describe( + "The location to get the weather for. By default, this is set to Greenwich, London." + ) + .default("Greenwich, London") + .optional(), + units: z + .string() + .describe( + "The units to use for the temperature (metric, imperial, or scientific). By default, this is set to metric." + ) + .default("metric") + .optional(), + }), + func: async ({ units, location }) => { + const unitArg = Object.keys(unitSuffixes).find((u) => units?.includes(u)) + ? units + : "metric"; + + const pois = await fetch( + `https://nominatim.openstreetmap.org/search?addressdetails=1&q=${ + location || "Greenwich, London" + }&format=jsonv2&limit=1` + ) + .then((res) => res.json()) + .catch((err) => { + console.log(err); + return []; + }); + + if (pois.length === 0) { + return { + success: false, + messages: [ + { + role: "tool", + content: `The weather in ${location} is unknown`, + }, + ], + data: {}, + }; + } + + const weather = await fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${ + pois[0]?.lat + }&longitude=${ + pois[0]?.lon + }¤t=temperature_2m,is_day,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=1${ + unitArg === "imperial" || unitArg === "fahrenheit" + ? "&temperature_unit=fahrenheit" + : "" + }` + ) + .then((res) => res.json()) + .catch((err) => { + console.log(err); + return {}; + }); + + const isDay = weather?.current?.is_day === 1; + + const isKelvin = unitArg === "kelvin" || unitArg === "scientific"; + + const infoMap = new Map( + Object.entries({ + current: isKelvin + ? weather?.current?.temperature_2m || 0 + 273.15 + : weather?.current?.temperature_2m, + currentWeather: + wmo[weather?.current?.weather_code]?.[isDay ? "day" : "night"], + todayWeather: + wmo[weather?.daily?.weather_code?.[0]]?.[isDay ? "day" : "night"], + high: isKelvin + ? (weather?.daily?.temperature_2m_max?.[0] || 0) + 273.15 + : weather?.daily?.temperature_2m_max?.[0], + low: isKelvin + ? (weather?.daily?.temperature_2m_min?.[0] || 0) + 273.15 + : weather?.daily?.temperature_2m_min?.[0], + }) + ); + + const formatMap = new Map( + Object.entries({ + current: `${infoMap.get("current") || ""}${ + unitSuffixes[unitArg || "metric"]?.temperature + }`, + currentWeather: + infoMap.get("currentWeather")?.description || "", + todayWeather: infoMap.get("todayWeather")?.description || "", + high: `${infoMap.get("high") || ""}${ + unitSuffixes[unitArg || "metric"]?.temperature + }`, + low: `${infoMap.get("low") || ""}${ + unitSuffixes[unitArg || "metric"]?.temperature + }`, + }) + ); + + return { + content: dedent` + The weather in ${ + pois[0]?.display_name + } is currently ${formatMap.get( + "current" + )} and ${formatMap.get("currentWeather")}. + + Today, the weather is expected to be ${formatMap.get( + "todayWeather" + )}, with a high of ${formatMap.get( + "high" + )} and a low of ${formatMap.get("low")}. + `, + }; + }, + }); diff --git a/packages/pwse-weather/localisation.js b/packages/pwse-weather/localisation.js new file mode 100644 index 0000000..cd18f1c --- /dev/null +++ b/packages/pwse-weather/localisation.js @@ -0,0 +1,28 @@ +export const __loader_exclude = true; + +export const unitSuffixes = { + metric: { + temperature: "°C", + speed: "km/h", + pressure: "hPa", + distance: "km", + volume: "m³", + time: "s", + }, + imperial: { + temperature: "°F", + speed: "mph", + pressure: "inHg", + distance: "mi", + volume: "ft³", + time: "min", + }, + scientific: { + temperature: "K", + speed: "m/s", + pressure: "Pa", + distance: "m", + volume: "m³", + time: "s", + }, +}; diff --git a/packages/pwse-weather/package.json b/packages/pwse-weather/package.json new file mode 100644 index 0000000..b58c8b4 --- /dev/null +++ b/packages/pwse-weather/package.json @@ -0,0 +1,14 @@ +{ + "name": "pwse-weather", + "module": "index.js", + "type": "module", + "exports": { + ".": "./index.js" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/packages/pwse-weather/tsconfig.json b/packages/pwse-weather/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/packages/pwse-weather/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "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 + } +} diff --git a/packages/pwse-weather/wmo.data.js b/packages/pwse-weather/wmo.data.js new file mode 100644 index 0000000..e1f19eb --- /dev/null +++ b/packages/pwse-weather/wmo.data.js @@ -0,0 +1,283 @@ +export const __loader_exclude = true; +export const codes = { + 0: { + day: { + description: "Sunny", + image: "http://openweathermap.org/img/wn/01d@2x.png", + }, + night: { + description: "Clear", + image: "http://openweathermap.org/img/wn/01n@2x.png", + }, + }, + 1: { + day: { + description: "Mainly Sunny", + image: "http://openweathermap.org/img/wn/01d@2x.png", + }, + night: { + description: "Mainly Clear", + image: "http://openweathermap.org/img/wn/01n@2x.png", + }, + }, + 2: { + day: { + description: "Partly Cloudy", + image: "http://openweathermap.org/img/wn/02d@2x.png", + }, + night: { + description: "Partly Cloudy", + image: "http://openweathermap.org/img/wn/02n@2x.png", + }, + }, + 3: { + day: { + description: "Cloudy", + image: "http://openweathermap.org/img/wn/03d@2x.png", + }, + night: { + description: "Cloudy", + image: "http://openweathermap.org/img/wn/03n@2x.png", + }, + }, + 45: { + day: { + description: "Foggy", + image: "http://openweathermap.org/img/wn/50d@2x.png", + }, + night: { + description: "Foggy", + image: "http://openweathermap.org/img/wn/50n@2x.png", + }, + }, + 48: { + day: { + description: "Rime Fog", + image: "http://openweathermap.org/img/wn/50d@2x.png", + }, + night: { + description: "Rime Fog", + image: "http://openweathermap.org/img/wn/50n@2x.png", + }, + }, + 51: { + day: { + description: "Light Drizzle", + image: "http://openweathermap.org/img/wn/09d@2x.png", + }, + night: { + description: "Light Drizzle", + image: "http://openweathermap.org/img/wn/09n@2x.png", + }, + }, + 53: { + day: { + description: "Drizzle", + image: "http://openweathermap.org/img/wn/09d@2x.png", + }, + night: { + description: "Drizzle", + image: "http://openweathermap.org/img/wn/09n@2x.png", + }, + }, + 55: { + day: { + description: "Heavy Drizzle", + image: "http://openweathermap.org/img/wn/09d@2x.png", + }, + night: { + description: "Heavy Drizzle", + image: "http://openweathermap.org/img/wn/09n@2x.png", + }, + }, + 56: { + day: { + description: "Light Freezing Drizzle", + image: "http://openweathermap.org/img/wn/09d@2x.png", + }, + night: { + description: "Light Freezing Drizzle", + image: "http://openweathermap.org/img/wn/09n@2x.png", + }, + }, + 57: { + day: { + description: "Freezing Drizzle", + image: "http://openweathermap.org/img/wn/09d@2x.png", + }, + night: { + description: "Freezing Drizzle", + image: "http://openweathermap.org/img/wn/09n@2x.png", + }, + }, + 61: { + day: { + description: "Light Rain", + image: "http://openweathermap.org/img/wn/10d@2x.png", + }, + night: { + description: "Light Rain", + image: "http://openweathermap.org/img/wn/10n@2x.png", + }, + }, + 63: { + day: { + description: "Rain", + image: "http://openweathermap.org/img/wn/10d@2x.png", + }, + night: { + description: "Rain", + image: "http://openweathermap.org/img/wn/10n@2x.png", + }, + }, + 65: { + day: { + description: "Heavy Rain", + image: "http://openweathermap.org/img/wn/10d@2x.png", + }, + night: { + description: "Heavy Rain", + image: "http://openweathermap.org/img/wn/10n@2x.png", + }, + }, + 66: { + day: { + description: "Light Freezing Rain", + image: "http://openweathermap.org/img/wn/10d@2x.png", + }, + night: { + description: "Light Freezing Rain", + image: "http://openweathermap.org/img/wn/10n@2x.png", + }, + }, + 67: { + day: { + description: "Freezing Rain", + image: "http://openweathermap.org/img/wn/10d@2x.png", + }, + night: { + description: "Freezing Rain", + image: "http://openweathermap.org/img/wn/10n@2x.png", + }, + }, + 71: { + day: { + description: "Light Snow", + image: "http://openweathermap.org/img/wn/13d@2x.png", + }, + night: { + description: "Light Snow", + image: "http://openweathermap.org/img/wn/13n@2x.png", + }, + }, + 73: { + day: { + description: "Snow", + image: "http://openweathermap.org/img/wn/13d@2x.png", + }, + night: { + description: "Snow", + image: "http://openweathermap.org/img/wn/13n@2x.png", + }, + }, + 75: { + day: { + description: "Heavy Snow", + image: "http://openweathermap.org/img/wn/13d@2x.png", + }, + night: { + description: "Heavy Snow", + image: "http://openweathermap.org/img/wn/13n@2x.png", + }, + }, + 77: { + day: { + description: "Snow Grains", + image: "http://openweathermap.org/img/wn/13d@2x.png", + }, + night: { + description: "Snow Grains", + image: "http://openweathermap.org/img/wn/13n@2x.png", + }, + }, + 80: { + day: { + description: "Light Showers", + image: "http://openweathermap.org/img/wn/09d@2x.png", + }, + night: { + description: "Light Showers", + image: "http://openweathermap.org/img/wn/09n@2x.png", + }, + }, + 81: { + day: { + description: "Showers", + image: "http://openweathermap.org/img/wn/09d@2x.png", + }, + night: { + description: "Showers", + image: "http://openweathermap.org/img/wn/09n@2x.png", + }, + }, + 82: { + day: { + description: "Heavy Showers", + image: "http://openweathermap.org/img/wn/09d@2x.png", + }, + night: { + description: "Heavy Showers", + image: "http://openweathermap.org/img/wn/09n@2x.png", + }, + }, + 85: { + day: { + description: "Light Snow Showers", + image: "http://openweathermap.org/img/wn/13d@2x.png", + }, + night: { + description: "Light Snow Showers", + image: "http://openweathermap.org/img/wn/13n@2x.png", + }, + }, + 86: { + day: { + description: "Snow Showers", + image: "http://openweathermap.org/img/wn/13d@2x.png", + }, + night: { + description: "Snow Showers", + image: "http://openweathermap.org/img/wn/13n@2x.png", + }, + }, + 95: { + day: { + description: "Thunderstorm", + image: "http://openweathermap.org/img/wn/11d@2x.png", + }, + night: { + description: "Thunderstorm", + image: "http://openweathermap.org/img/wn/11n@2x.png", + }, + }, + 96: { + day: { + description: "Light Thunderstorms With Hail", + image: "http://openweathermap.org/img/wn/11d@2x.png", + }, + night: { + description: "Light Thunderstorms With Hail", + image: "http://openweathermap.org/img/wn/11n@2x.png", + }, + }, + 99: { + day: { + description: "Thunderstorm With Hail", + image: "http://openweathermap.org/img/wn/11d@2x.png", + }, + night: { + description: "Thunderstorm With Hail", + image: "http://openweathermap.org/img/wn/11n@2x.png", + }, + }, +}; From a7cca37b9a16b15f77b4ffb4d87d01875381a208 Mon Sep 17 00:00:00 2001 From: Dani Date: Fri, 2 Aug 2024 15:56:51 -0400 Subject: [PATCH 3/4] we continue cooking up what is possibly the most horribly-typed and horribly-written typescript/langchain api in existence --- apps/core/src/lib/Integrations.ts | 6 ++-- apps/core/src/lib/LangChain.ts | 25 +++++++++++----- apps/core/src/lib/globals.ts | 45 +++++++++++++++++++++------- apps/core/src/routes/chat/create.ts | 36 ++++++++++++++++------ apps/core/src/routes/chat/history.ts | 27 +++++++++++++++++ 5 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 apps/core/src/routes/chat/history.ts diff --git a/apps/core/src/lib/Integrations.ts b/apps/core/src/lib/Integrations.ts index 3a8abee..a2f5003 100644 --- a/apps/core/src/lib/Integrations.ts +++ b/apps/core/src/lib/Integrations.ts @@ -3,7 +3,6 @@ import type { Chat, ChatMessage, HistoryMessage } from "./LangChain" import { config, IORedis, providers } from "./globals" import z from "zod"; import { AIMessageChunk } from "@langchain/core/messages"; -import { convertToOpenAITool } from "@langchain/core/utils/function_calling"; import { nanoid } from "nanoid"; import zodToJsonSchema from "zod-to-json-schema"; import type { ChatOllama } from "@langchain/ollama"; @@ -127,9 +126,8 @@ export class IntegrationsRunner { async invoke(messages: HistoryMessage[]): Promise<{ chatCompletion: AIMessageChunk, tasks: IntegrationRequest[] }> { const ctx: ChatMessage[] = this._chat.history.transform(messages); - const ifm = this._langChainAccessor.bind({ - tools: config.integrations.map(convertToOpenAITool), - parallel_tool_calls: false + const ifm = this._langChainAccessor.bindTools(config.integrations).bind({ + parallel_tool_calls: false, }); const out = await ifm.invoke(ctx).catch((err) => { diff --git a/apps/core/src/lib/LangChain.ts b/apps/core/src/lib/LangChain.ts index debbcf4..f30f790 100644 --- a/apps/core/src/lib/LangChain.ts +++ b/apps/core/src/lib/LangChain.ts @@ -14,6 +14,8 @@ export interface ChatOptions { callerProvider: string; callerModel: string; conversationId: string; + onlineProvider?: string; + onlineModel?: string; } export enum ChatRole { @@ -153,7 +155,7 @@ export class History { content: v.content, name: v.toolCalled?.name, additional_kwargs: v.toolCalled?.args - }); + }) } else { return new AIMessage({ content: v.content, @@ -169,28 +171,34 @@ export class Chat { protected _model: string; protected _callerProvider: string; protected _callerModel: string; + protected _onlineProvider: string; + protected _onlineModel: string; conversationId: string; langChainAccessor: ChatOpenAI | ChatOllama; + onlineLangChainAccessor: ChatOpenAI | ChatOllama; history: History; integrations: IntegrationsRunner; - constructor({ provider, model, callerProvider, callerModel, conversationId }: ChatOptions) { + constructor({ provider, model, callerProvider, callerModel, onlineProvider, onlineModel, conversationId }: ChatOptions) { this._provider = provider ?? process.env.LANGCHAIN_PROVIDER ?? "openai"; this._model = model ?? process.env.LANGCHAIN_MODEL ?? "gpt-4o-mini"; this._callerProvider = callerProvider ?? process.env.LANGCHAIN_CALLER_PROVIDER ?? "openai"; this._callerModel = callerModel ?? process.env.LANGCHAIN_CALLER_MODEL ?? "gpt-4o-mini"; + this._onlineProvider = onlineProvider ?? process.env.LANGCHAIN_ONLINE_CALLER_PROVIDER ?? "openai"; + this._onlineModel = onlineModel ?? process.env.LANGCHAIN_ONLINE_CALLER_MODEL ?? "gpt-4o-mini"; this.conversationId = conversationId; this.langChainAccessor = providers[this._provider].generator(this._model); + this.onlineLangChainAccessor = (onlineProvider && onlineModel) ? providers[this._onlineProvider].generator(this._onlineModel) : this.langChainAccessor; this.history = new History(IORedis, conversationId); this.integrations = new IntegrationsRunner(IORedis, this, this._callerProvider, this._callerModel); } - private async _invoke(messages: HistoryMessage[]): Promise { + private async _invoke(accessor: ChatOpenAI | ChatOllama, messages: HistoryMessage[], tools: boolean = false): Promise { const ctx: ChatMessage[] = this.history.transform(messages); const lastMsg: HistoryMessage = messages[messages.length - 1]; - const chatCompletion: AIMessageChunk = await this.langChainAccessor.bind({ - tools: config.integrations as any + const chatCompletion: AIMessageChunk = await accessor.bind({ + //tools: tools ? config.integrations as any : undefined }).invoke(ctx); await this.history.add([ @@ -238,7 +246,7 @@ export class Chat { }; }); - const completedCtx = await this._invoke([ + const completedCtx = await this._invoke(this.onlineLangChainAccessor, [ ...untransformedCtx, { role: ChatRole.Assistant, @@ -253,14 +261,15 @@ export class Chat { })) }, ...taskMessages - ]); + ], true); return { ...completedCtx, taskResults: tasks }; } else { - return chatCompletion; + const regular: AIMessageChunk = await this._invoke(this.langChainAccessor, untransformedCtx, false); + return regular; } } } \ No newline at end of file diff --git a/apps/core/src/lib/globals.ts b/apps/core/src/lib/globals.ts index be64107..b15531e 100644 --- a/apps/core/src/lib/globals.ts +++ b/apps/core/src/lib/globals.ts @@ -77,11 +77,11 @@ const generateRedisClient = async () => { }); IORedis.on("connect", () => { - console.log(chalk.bold(`${chalk.green("✔")} Connected to IORedis (History client)`)) + console.log(chalk.bold(`${chalk.green("✔ ")} Connected to IORedis (History client)`)) }); IORedis.on("error", (err) => { - console.error(chalk.bold(`${chalk.red("✖")} An error occurred while connecting to Redis`)); + console.error(chalk.bold(`${chalk.red("✖ ")} An error occurred while connecting to Redis`)); throw err; }); @@ -103,25 +103,50 @@ export const generateGlobalConfig = async () => { // attempt import of .ts first try { const configModule = await import(configFile); - console.log(chalk.bold(`${chalk.green("✔")} Loaded ${chalk.blue("pwse.config.ts")}`)); + console.log(chalk.bold(`${chalk.green("✔ ")} Loaded ${chalk.blue("pwse.config.ts")}`)); return configModule.default as Configuration; } catch (err) { // if import fails, attempt import of .js try { const configModule = await import(configFile.replace(".ts", ".js")); - console.log(chalk.bold(`${chalk.green("✔")} Loaded ${chalk.yellow("pwse.config.js")}`)); + console.log(chalk.bold(`${chalk.green("✔ ")} Loaded ${chalk.yellow("pwse.config.js")}`)); return configModule.default as Configuration; } catch (err) { // if import fails, throw error - throw new Error(dedent`An error occurred during the configuration load. - No configuration file was found in the apex directory, ${chalk.blue(configDir)}. - Please create one of the following files in this directory: - ${chalk.blue("pwse.config.ts")} in TypeScript - ${chalk.yellow("pwse.config.js")} in JavaScript - `); + const details = dedent` + ${chalk.blue("? ")} No configuration file was found in the apex directory, ${chalk.blue(configDir)}. + ${chalk.green("✔ ")} Please create one of the following files in this directory: + ${chalk.bold.blue(" T")} ${chalk.blue("pwse.config.ts")} + ${chalk.bold.yellow(" J")} ${chalk.yellow("pwse.config.js")} + `; + + console.log("=====") + console.error(dedent`${chalk.yellow("✖ ")} ${chalk.bold("An error occurred during the configuration load: No configuration file found")} + ${chalk.reset(details)}`); + + process.exit(1); } } } +export const configurationChecks = () => { + if (!process.env.OLLAMA_BASE_URL && process.env.DANGEROUS_DISABLE_LLAMA_GUARD !== "true") { + const details = dedent` + ${chalk.blue("? ")} Pathways Engine uses Ollama in order to moderate inputs and outputs within Chat and Text Completion. + ${chalk.green("✔ ")} ${chalk.bold("FIX")}: Set OLLAMA_BASE_URL to the URL of your Ollama instance in your environment. + ${chalk.yellow("‼ ")} ${chalk.bold("UNRECOMMENDED")}: Alternatively, you can disable this check by setting DANGEROUS_DISABLE_LLAMA_GUARD=true. This is not recommended. + `; + + console.log("=====") + console.error(dedent`${chalk.yellow("✖ ")} ${chalk.bold("An error occurred during the configuration load: OLLAMA_BASE_URL is not set")} + ${chalk.reset(details)}`); + + process.exit(1); + } + + return true; +} + +export const configValid = configurationChecks(); export const IORedis = await generateRedisClient(); export const config = await generateGlobalConfig(); \ No newline at end of file diff --git a/apps/core/src/routes/chat/create.ts b/apps/core/src/routes/chat/create.ts index 44dcc92..c5f0e85 100644 --- a/apps/core/src/routes/chat/create.ts +++ b/apps/core/src/routes/chat/create.ts @@ -2,29 +2,40 @@ import { nanoid } from "nanoid"; import type { ElysiaApp } from "../../index.ts"; import { Chat, ChatRole } from "../../lib/LangChain.ts"; +import { t } from "elysia"; -const Route = (app: ElysiaApp) => app.get('/', async (req) => { - if (!req.query.q) { +const Route = (app: ElysiaApp) => app.post('/', async (req) => { + if (!req.body) { return { status: 400, body: { - error: "Missing query parameter 'q'" + error: "Missing body" } } - } else if (!req.query.conversation) { + } else if (!req.body.message) { return { status: 400, body: { - error: "Missing query parameter 'conversation'" + error: "Missing body key 'q'" + } + } + } else if (!req.body.conversation) { + return { + status: 400, + body: { + error: "Missing body key 'conversation'" } } } + const chat = new Chat({ - provider: "openai", - model: "gpt-4o-mini", + provider: "ollama", + model: "llama3.1", callerProvider: "ollama", callerModel: "llama3.1", - conversationId: req.query.conversation.trim() ?? "test" + conversationId: req.body.conversation?.trim() ?? "test", + onlineProvider: "openrouter", + onlineModel: "perplexity/llama-3.1-sonar-small-128k-chat", }); const request: any = await chat.send({ @@ -33,7 +44,7 @@ const Route = (app: ElysiaApp) => app.get('/', async (req) => { id: nanoid(), timestamp: new Date().toISOString(), role: ChatRole.User, - content: req.query.q?.trim() ?? "Create an error message and tell the user to try using a proper query parameter", + content: req.body.message?.trim() ?? "Create an error message and tell the user to try using a proper query parameter", user: { id: nanoid(), name: "User", @@ -44,6 +55,7 @@ const Route = (app: ElysiaApp) => app.get('/', async (req) => { ] }); + return { content: request.content, metadata: request.response_metadata, @@ -54,6 +66,12 @@ const Route = (app: ElysiaApp) => app.get('/', async (req) => { total: request.usage_metadata?.total_tokens } } +}, { + body: t.Object({ + message: t.String(), + prompt: t.Optional(t.String()), + conversation: t.String() + }) }) diff --git a/apps/core/src/routes/chat/history.ts b/apps/core/src/routes/chat/history.ts new file mode 100644 index 0000000..6634d9c --- /dev/null +++ b/apps/core/src/routes/chat/history.ts @@ -0,0 +1,27 @@ +import type { ElysiaApp } from "../../index.ts"; +import { Chat } from "../../lib/LangChain.ts"; + +const Route = (app: ElysiaApp) => app.get('/', async (req) => { + if (!req.query.conversation) { + return { + status: 400, + body: { + error: "Missing query parameter 'conversation'" + } + } + } + const chat = new Chat({ + provider: "openai", + model: "gpt-4o-mini", + callerProvider: "ollama", + callerModel: "llama3.1", + conversationId: req.query.conversation.trim() ?? "test", + }); + + const request: any = await chat.history.getAll(); + + return request; +}) + + +export default Route; \ No newline at end of file From b8bb877e19cde6c9e076947334227c8684074c9f Mon Sep 17 00:00:00 2001 From: Dani Date: Thu, 14 Nov 2024 10:50:18 -0500 Subject: [PATCH 4/4] spaghetti code unite --- apps/api/src/routes/oai/{ => v1}/index.ts | 4 +- apps/core/src/lib/LangChain.ts | 12 +++-- apps/core/src/routes/chat/history.ts | 27 ---------- .../chat/complete.ts} | 10 ++-- .../chat/history/[conversation].ts | 47 ++++++++++++++++++ bun.lockb | Bin 53015 -> 71560 bytes package.json | 8 +-- 7 files changed, 67 insertions(+), 41 deletions(-) rename apps/api/src/routes/oai/{ => v1}/index.ts (86%) delete mode 100644 apps/core/src/routes/chat/history.ts rename apps/core/src/routes/{chat/create.ts => conversations/chat/complete.ts} (88%) create mode 100644 apps/core/src/routes/conversations/chat/history/[conversation].ts diff --git a/apps/api/src/routes/oai/index.ts b/apps/api/src/routes/oai/v1/index.ts similarity index 86% rename from apps/api/src/routes/oai/index.ts rename to apps/api/src/routes/oai/v1/index.ts index 01d5077..93b1fa6 100644 --- a/apps/api/src/routes/oai/index.ts +++ b/apps/api/src/routes/oai/v1/index.ts @@ -1,5 +1,5 @@ -import type { ElysiaApp } from "../../index.ts"; -import { SupportedCalls, type APIMetadataResponse, type APIVersion } from "../../types/global.ts"; +import type { ElysiaApp } from "../../../index.ts"; +import { SupportedCalls, type APIMetadataResponse, type APIVersion } from "../../../types/global.ts"; export const Metadata: APIVersion = { identifier: "v1", diff --git a/apps/core/src/lib/LangChain.ts b/apps/core/src/lib/LangChain.ts index f30f790..f44f409 100644 --- a/apps/core/src/lib/LangChain.ts +++ b/apps/core/src/lib/LangChain.ts @@ -7,6 +7,7 @@ import { HumanMessage, AIMessage, FunctionMessage, RemoveMessage, SystemMessage, import { nanoid } from "nanoid"; import { IntegrationsRunner } from "./Integrations.ts"; import type { IntegrationRequest } from "./Integrations"; +import { z } from "zod"; export interface ChatOptions { provider: string; @@ -164,6 +165,12 @@ export class History { } }); }; + + async clear(): Promise { + const keys = await this.allKeys(); + const toDelete = keys.map((v) => `${this.conversationId}:${v}`); + void await this._store.mdelete(toDelete); + } } export class Chat { @@ -196,10 +203,9 @@ export class Chat { private async _invoke(accessor: ChatOpenAI | ChatOllama, messages: HistoryMessage[], tools: boolean = false): Promise { const ctx: ChatMessage[] = this.history.transform(messages); const lastMsg: HistoryMessage = messages[messages.length - 1]; - const chatCompletion: AIMessageChunk = await accessor.bind({ - //tools: tools ? config.integrations as any : undefined - }).invoke(ctx); + tools: tools ? config.integrations as any : undefined + }).invoke(tools ? [new SystemMessage("Process the user's integration accurately. If the integration is incorrect or you cannot fulfill the query, apologize to the user and state: 'I'm sorry, I couldn't find what you were looking for.' Do not attempt any action beyond the scope of the provided integration. Follow this instruction strictly."), ...ctx] : ctx); await this.history.add([ { diff --git a/apps/core/src/routes/chat/history.ts b/apps/core/src/routes/chat/history.ts deleted file mode 100644 index 6634d9c..0000000 --- a/apps/core/src/routes/chat/history.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ElysiaApp } from "../../index.ts"; -import { Chat } from "../../lib/LangChain.ts"; - -const Route = (app: ElysiaApp) => app.get('/', async (req) => { - if (!req.query.conversation) { - return { - status: 400, - body: { - error: "Missing query parameter 'conversation'" - } - } - } - const chat = new Chat({ - provider: "openai", - model: "gpt-4o-mini", - callerProvider: "ollama", - callerModel: "llama3.1", - conversationId: req.query.conversation.trim() ?? "test", - }); - - const request: any = await chat.history.getAll(); - - return request; -}) - - -export default Route; \ No newline at end of file diff --git a/apps/core/src/routes/chat/create.ts b/apps/core/src/routes/conversations/chat/complete.ts similarity index 88% rename from apps/core/src/routes/chat/create.ts rename to apps/core/src/routes/conversations/chat/complete.ts index c5f0e85..fee5f8f 100644 --- a/apps/core/src/routes/chat/create.ts +++ b/apps/core/src/routes/conversations/chat/complete.ts @@ -1,7 +1,7 @@ import { nanoid } from "nanoid"; -import type { ElysiaApp } from "../../index.ts"; -import { Chat, ChatRole } from "../../lib/LangChain.ts"; +import type { ElysiaApp } from "../../../index.ts"; +import { Chat, ChatRole } from "../../../lib/LangChain.ts"; import { t } from "elysia"; const Route = (app: ElysiaApp) => app.post('/', async (req) => { @@ -30,12 +30,12 @@ const Route = (app: ElysiaApp) => app.post('/', async (req) => { const chat = new Chat({ provider: "ollama", - model: "llama3.1", + model: "llama3.2-vision:11b", callerProvider: "ollama", - callerModel: "llama3.1", + callerModel: "llama3.2:3b", conversationId: req.body.conversation?.trim() ?? "test", onlineProvider: "openrouter", - onlineModel: "perplexity/llama-3.1-sonar-small-128k-chat", + onlineModel: "anthropic/claude-3-haiku", }); const request: any = await chat.send({ diff --git a/apps/core/src/routes/conversations/chat/history/[conversation].ts b/apps/core/src/routes/conversations/chat/history/[conversation].ts new file mode 100644 index 0000000..6cb24e2 --- /dev/null +++ b/apps/core/src/routes/conversations/chat/history/[conversation].ts @@ -0,0 +1,47 @@ +import type { ElysiaApp } from "../../../../index.ts"; +import { Chat } from "../../../../lib/LangChain.ts"; + +const GET = (app: ElysiaApp) => app.get("/", async (req) => { + const params: { conversation: string } = req.params; + + const chat = new Chat({ + provider: "openai", + model: "gpt-4o-mini", + callerProvider: "ollama", + callerModel: "llama3.2-vision:11b", + conversationId: params.conversation.trim() ?? "test", + }); + + const request: any = await chat.history.getAll(); + + return { + status: 200, + response: request + } +}); + +const DELETE = (app: ElysiaApp) => app.delete("/", async (req) => { + const params: { conversation: string } = req.params; + + const chat = new Chat({ + provider: "openai", + model: "gpt-4o-mini", + callerProvider: "ollama", + callerModel: "llama3.2:3b", + conversationId: params.conversation.trim() ?? "test", + }); + + await chat.history.clear(); + + const request: any = await chat.history.getAll(); + + return { + status: 200, + response: request + } +}); + +const Route = (app: ElysiaApp) => DELETE(GET(app)) + + +export default Route; \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 8f47f4182a8f3328c415ca4fe268c3b774d415ca..7b8933a34154e2e321dab680079b884173ade60d 100644 GIT binary patch delta 16715 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 X*M@XBGlmpww;>^CSJarF)8YLO*1cw! delta 5861 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