├── .DS_Store ├── .gitignore ├── .npmignore ├── .reason.config.js ├── README.md ├── ast-transforms ├── actions │ ├── action-observervability.js │ ├── action-prompt-info.js │ ├── actionError.js │ ├── actionObserver.ts │ └── actions.js ├── agents │ ├── agent-observability.js │ ├── agent-prompt-info.js │ ├── agent-transform.js │ ├── agentError.js │ └── agentObserver.ts ├── getBasePath.js ├── internalname.js ├── isPathFromReason.js ├── think │ ├── think-prompt-info.js │ ├── think-stream-prompt-info.js │ ├── think-transform.js │ └── thinkError.js └── utils │ ├── get-imports.js │ ├── get-ts-types.js │ ├── isJs.js │ ├── jsdoc-parser.js │ ├── to-json-schema.js │ └── to-ts-ast.js ├── bun.lockb ├── commands ├── reason-command.js └── tryreason-command.js ├── compiler └── ts-dev-mode-loader.mjs ├── configs ├── anyscale.ts ├── openai.ts └── reason-config.ts ├── functions ├── __internal │ ├── StremableObject.ts │ ├── __internal_agent.ts │ ├── __internal_think.ts │ └── think-extractor.ts ├── agent.ts ├── index.ts ├── stream.ts └── think.ts ├── observability ├── context.d.ts ├── createContext.ts ├── setup-opentel.ts └── tracer.ts ├── package-lock.json ├── package.json ├── scripts └── post-install.js ├── server ├── StreamEncoder.ts ├── entrypoints.ts ├── error-handler.ts ├── fetch-standard.ts ├── handlers │ ├── asyncFunctionHandler.ts │ ├── asyncGeneratorHandler.ts │ ├── entrypointHandler.ts │ └── functionHandler.ts ├── index.ts ├── server.ts ├── serverError.ts └── setup.ts ├── services ├── anyscale │ ├── getChatCompletion.ts │ └── getFunctionCompletion.ts ├── db.ts ├── getChatCompletion.ts ├── getFunctionCompletion.ts └── openai │ ├── getChatCompletion.ts │ └── getFunctionCompletion.ts ├── sqlite ├── .DS_Store ├── sqlite-dialect-config.ts ├── sqlite-dialect.ts └── sqlite-driver.ts ├── tests ├── ast-transforms │ └── utils │ │ └── to-json-schema.spec.ts ├── server │ ├── StreamEncoder.spec.ts │ └── entrypoints.spec.ts └── utils │ └── complete-json.spec.ts ├── tsconfig.json ├── tsconfig.tsbuildinfo ├── types ├── actionConfig.d.ts ├── agentConfig.d.ts ├── db │ ├── agent-history.d.ts │ └── database.d.ts ├── iagent.d.ts ├── oai-chat-models.d.ts ├── reasonConfig.d.ts ├── server.d.ts ├── streamable.d.ts └── thinkConfig.d.ts ├── utils ├── asyncLocalStorage.ts ├── complete-json.ts ├── isDebug.js ├── isDebug.ts ├── libname.js ├── oai-function.ts └── reasonError.js └── web ├── .gitignore ├── README.md ├── a.ts ├── app ├── entrypoint │ └── [entrypoint] │ │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── bun.lockb ├── components.json ├── components ├── GradientHeading.tsx ├── ReasonNotFound.tsx ├── ReasonTitle │ ├── dynamic.tsx │ ├── index.tsx │ └── static.tsx ├── entrypoint │ ├── ResponseVisualizer.tsx │ ├── chat │ │ └── ActionVisualizer.tsx │ └── request-information │ │ ├── Body.tsx │ │ ├── Header.tsx │ │ └── RequestInfo.tsx └── ui │ ├── accordion.tsx │ ├── button.tsx │ ├── card.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── switch.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── tooltip.tsx │ └── use-toast.ts ├── gradients.ts ├── hooks ├── useChat.tsx └── useReason.tsx ├── lib └── utils.ts ├── next.config.js ├── package copy.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── bg4.jpg ├── reason ├── ReasonError.ts └── StreamDecoder.ts ├── tailwind.config.js ├── tailwind.config.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/try-reason/reason/113046a45d8cceea6ac6cfe87af77565fdabf06a/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package 4 | **.tgz 5 | web/.next -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **.tgz 2 | node_modules 3 | 4 | web/node_modules 5 | web/app 6 | web/components 7 | web/hooks 8 | web/lib 9 | web/reason 10 | web/components.json 11 | web/README.md 12 | web/tailwind.config.js 13 | web/tailwind.config.ts 14 | web/postcss.config.js 15 | web/tsconfig.json 16 | 17 | /* 18 | 19 | !/dist 20 | !/scripts 21 | !/web 22 | !/sample 23 | !/sample/.reason.config.js 24 | !/package.json -------------------------------------------------------------------------------- /.reason.config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/try-reason/reason/113046a45d8cceea6ac6cfe87af77565fdabf06a/.reason.config.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### ⚠️ RΞASON is now deprecated and probably not working due to changes in the OpenAI API. 2 | 3 | ![REASON](https://tryreason.b-cdn.net/icon2.webp) 4 | 5 |

RΞASON

6 |

7 | The minimalistic Typescript framework for building great LLM apps. 8 |

9 | 10 | ```bash 11 | npx use-reason@latest 12 | ``` 13 | A small demo: 14 | ```ts 15 | import { reason } from 'tryreason' 16 | 17 | interface Joke { 18 | /** Use this property to indicate the age rating of the joke */ 19 | rating: number; 20 | joke: string; 21 | 22 | /** Use this property to explain the joke to those who did not understood it */ 23 | explanation: string; 24 | } 25 | 26 | const joke = await reason('tell me a really spicy joke') 27 | ``` 28 | The value of the `joke` object is: 29 | ```json 30 | { 31 | "joke": "I'd tell you a chemistry joke but I know I wouldn't get a reaction.", 32 | "rating": 18, 33 | "explanation": "This joke is a play on words. The term 'reaction' refers to both a chemical process and a response from someone. The humor comes from the double meaning, implying that the joke might not be funny enough to elicit a response." 34 | } 35 | ``` 36 | 37 | Yep, RΞASON *actually* utilizes your Typescript type information as the guide for the LLM. This is a **key distinction**: RΞASON uses Typescript (& JSDoc comments) at runtime to help the LLM know what to return. 38 | 39 | ## Getting started 40 | Head over to https://docs.tryreason.dev to get started & learn more. 41 | -------------------------------------------------------------------------------- /ast-transforms/actions/action-observervability.js: -------------------------------------------------------------------------------- 1 | import getBasePath from '../getBasePath.js'; 2 | 3 | function getActionObserverPath() { 4 | return `${getBasePath()}actions/actionObserver.js`; 5 | } 6 | 7 | const wrappedFunctionPath = getActionObserverPath(); 8 | 9 | const wrapWithActionObserver = ({ types: t }) => ({ 10 | name: "wrap-default-export", 11 | visitor: { 12 | Program: { 13 | enter(path) { 14 | const importDeclaration = t.importDeclaration( 15 | [t.importDefaultSpecifier(t.identifier('actionObserver'))], 16 | t.stringLiteral(wrappedFunctionPath) 17 | ); 18 | path.node.body.unshift(importDeclaration); 19 | } 20 | }, 21 | ExportDefaultDeclaration(path) { 22 | let declaration = path.node.declaration; 23 | 24 | if (t.isIdentifier(path.node.declaration)) { 25 | const binding = path.scope.getBinding(path.node.declaration.name); 26 | if (!binding || !t.isFunctionDeclaration(binding.path.node)) throw new ReasonError('Inside an action file, the default export must be a function declaration.', 9); 27 | declaration = binding.path.node; 28 | } else if (t.isFunctionDeclaration(path.node.declaration)) { 29 | declaration = path.node.declaration; 30 | } else { 31 | throw new ReasonError('Inside an action file, the default export must be a function declaration.', 10) 32 | } 33 | 34 | // If the exported value is a FunctionDeclaration, convert it to a FunctionExpression 35 | if (t.isFunctionDeclaration(declaration)) { 36 | declaration = t.functionExpression( 37 | declaration.id, 38 | declaration.params, 39 | declaration.body, 40 | declaration.generator, 41 | declaration.async 42 | ); 43 | } 44 | 45 | // If the exported value is now a FunctionExpression or an ArrowFunctionExpression, wrap it 46 | if (t.isFunctionExpression(declaration) || t.isArrowFunctionExpression(declaration)) { 47 | const fooIdentifier = t.identifier('actionObserver'); 48 | const wrappedFunction = t.callExpression(fooIdentifier, [declaration]); 49 | path.node.declaration = wrappedFunction; 50 | } 51 | } 52 | } 53 | }); 54 | 55 | 56 | export default wrapWithActionObserver -------------------------------------------------------------------------------- /ast-transforms/actions/actionError.js: -------------------------------------------------------------------------------- 1 | import isDebug from "../../utils/isDebug.js"; 2 | 3 | export default class ActionError extends Error { 4 | name; 5 | description; 6 | code; 7 | 8 | constructor(description, code, debug_info = null) { 9 | const message = `ActionError: ${description}\nError code: ${code}` 10 | super(message) 11 | 12 | this.name = 'ActionError' 13 | this.description = description 14 | this.code = code 15 | 16 | if (isDebug) { 17 | console.log('RΞASON — INTERNAL DEBUG INFORMATION:'); 18 | console.log(debug_info); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /ast-transforms/actions/actionObserver.ts: -------------------------------------------------------------------------------- 1 | import IContext from "../../observability/context" 2 | import { Trace } from "../../observability/tracer" 3 | import asyncLocalStorage from "../../utils/asyncLocalStorage" 4 | 5 | const start: EventHandler = (fn: Function) => { 6 | // console.time(fn.name) 7 | } 8 | 9 | const end: EventHandler = (fn: Function) => { 10 | // console.timeEnd(fn.name) 11 | } 12 | 13 | const isStream: EventHandler = (fn: Function) => { 14 | // console.log(fn.name, ' is stream action!') 15 | } 16 | 17 | const streamNewByte: EventHandler = (fn: Function) => { 18 | // console.log(fn.name, ' new byte!') 19 | } 20 | 21 | type EventHandler = (...args: any[]) => void 22 | const events = { 23 | 'start': start, 24 | 'end': end, 25 | 'stream-detected': isStream, 26 | 'stream-new-byte': streamNewByte, 27 | } 28 | 29 | function observe(event: keyof typeof events, ...args: any[]) { 30 | events[event](...args) 31 | } 32 | 33 | export default function actionObserver(fn: Function) { 34 | switch (fn.constructor.name) { 35 | case 'Function': { 36 | return function (...args: any[]) { 37 | const context = asyncLocalStorage.getStore() as IContext 38 | const trace = new Trace(context, `[ACTION] ${fn.name}`, 'action') 39 | 40 | return trace.startActiveSpanSync((span: any) => { 41 | trace.addAttribute('action.name', fn.name) 42 | trace.addAttribute('action.input', args) 43 | observe('start', fn) 44 | try { 45 | let result = fn(...args) 46 | 47 | trace.addAttribute('action.output', result) 48 | observe('end', fn) 49 | trace.end() 50 | return result 51 | } catch (err) { 52 | trace.err(err) 53 | throw err 54 | } 55 | }) 56 | } 57 | } 58 | 59 | case 'AsyncFunction': { 60 | return async function (...args: any[]) { 61 | const context = asyncLocalStorage.getStore() as IContext 62 | const trace = new Trace(context, `[ACTION] ${fn.name}`, 'action') 63 | 64 | return trace.startActiveSpan(async (span: any) => { 65 | trace.addAttribute('action.name', fn.name) 66 | trace.addAttribute('action.input', args) 67 | trace.addAttribute('action.input', args) 68 | observe('start', fn) 69 | try { 70 | let result = await fn(...args) 71 | trace.addAttribute('action.output', result) 72 | observe('end', fn) 73 | trace.end() 74 | return result 75 | } catch (err) { 76 | trace.err(err) 77 | throw err 78 | } 79 | 80 | }) 81 | } 82 | } 83 | 84 | // TODO: telemetry: how to observe this? 85 | case 'AsyncGeneratorFunction': { 86 | return async function* (...args: any[]) { 87 | observe('stream-detected', fn) 88 | observe('start', fn) 89 | 90 | const gen = fn(...args) 91 | let result = await gen.next() 92 | while (!result.done) { 93 | let { value, done } = result 94 | 95 | yield value 96 | 97 | result = await gen.next() 98 | } 99 | 100 | observe('end', fn) 101 | return result.value 102 | } 103 | } 104 | 105 | case 'GeneratorFunction': { 106 | return async function* (...args: any[]) { 107 | observe('stream-detected', fn) 108 | observe('start', fn) 109 | 110 | const gen = fn(...args) 111 | let result = gen.next() 112 | while (!result.done) { 113 | let { value, done } = result 114 | 115 | yield value 116 | 117 | result = gen.next() 118 | } 119 | 120 | observe('end', fn) 121 | return result.value 122 | } 123 | } 124 | } 125 | } 126 | 127 | // asyncStores[actionId] = asyncStore 128 | 129 | // i could create a new store with a newly generated actionId 130 | // but how would i be able to get that id in child actions? 131 | // asyncStores have the nice property of literally only existing in the current execution context -------------------------------------------------------------------------------- /ast-transforms/actions/actions.js: -------------------------------------------------------------------------------- 1 | import { transformAsync } from '@babel/core'; 2 | import isPathFromReason from '../isPathFromReason.js'; 3 | import wrapWithActionObserver from "./action-observervability.js"; 4 | import getPromptInfoFromAction from './action-prompt-info.js'; 5 | import getPromptInfoFromThink from '../think/think-prompt-info.js' 6 | import getPromptInfoFromThinkStream from '../think/think-stream-prompt-info.js' 7 | import isJs from '../utils/isJs.js'; 8 | 9 | 10 | export default async function transformAction(code, filePath) { 11 | const extractPromptInfoPlugin = getPromptInfoFromAction(code, filePath) 12 | 13 | let plugins = [extractPromptInfoPlugin] 14 | 15 | if (isPathFromReason(code) && code.includes('reason')) { 16 | // can potentially use `think` 17 | const thinkPlugin = getPromptInfoFromThink(code, filePath, isJs(filePath)) 18 | plugins.push(thinkPlugin) 19 | } 20 | 21 | if (isPathFromReason(code) && code.includes('reasonStream')) { 22 | // can potentially use `think` 23 | const thinkPlugin = getPromptInfoFromThinkStream(code, filePath, isJs(filePath)) 24 | plugins.push(thinkPlugin) 25 | } 26 | 27 | plugins.push(wrapWithActionObserver) 28 | 29 | const result = await transformAsync(code, { 30 | presets: [ 31 | ['@babel/preset-typescript', { allExtensions: true, isTSX: false }] 32 | ], 33 | plugins, 34 | // filename: filePath, 35 | sourceType: 'module', 36 | }); 37 | 38 | return result.code 39 | } -------------------------------------------------------------------------------- /ast-transforms/agents/agent-observability.js: -------------------------------------------------------------------------------- 1 | import getBasePath from '../getBasePath.js'; 2 | 3 | function getAgentObserverPath() { 4 | return `${getBasePath()}agents/agentObserver.js`; 5 | } 6 | 7 | const wrappedFunctionPath = getAgentObserverPath(); 8 | 9 | const wrapWithAgentObserver = ({ types: t }) => ({ 10 | name: "wrap-default-export", 11 | visitor: { 12 | Program: { 13 | enter(path) { 14 | const importDeclaration = t.importDeclaration( 15 | [t.importDefaultSpecifier(t.identifier('agentObserver'))], 16 | t.stringLiteral(wrappedFunctionPath) 17 | ); 18 | path.node.body.unshift(importDeclaration); 19 | } 20 | }, 21 | ExportDefaultDeclaration(path) { 22 | let declaration = path.node.declaration; 23 | 24 | if (t.isIdentifier(path.node.declaration)) { 25 | const binding = path.scope.getBinding(path.node.declaration.name); 26 | if (!binding || !t.isFunctionDeclaration(binding.path.node)) throw new ReasonError('Inside an agent, the default export must be a function declaration.', 30); 27 | declaration = binding.path.node; 28 | } else if (t.isFunctionDeclaration(path.node.declaration)) { 29 | declaration = path.node.declaration; 30 | } else { 31 | throw new ReasonError('Inside an agent file, the default export must be a function declaration.', 31) 32 | } 33 | 34 | // If the exported value is a FunctionDeclaration, convert it to a FunctionExpression 35 | if (t.isFunctionDeclaration(declaration)) { 36 | declaration = t.functionExpression( 37 | declaration.id, 38 | declaration.params, 39 | declaration.body, 40 | declaration.generator, 41 | declaration.async 42 | ); 43 | } 44 | 45 | // If the exported value is now a FunctionExpression or an ArrowFunctionExpression, wrap it 46 | if (t.isFunctionExpression(declaration) || t.isArrowFunctionExpression(declaration)) { 47 | const fooIdentifier = t.identifier('agentObserver'); 48 | const wrappedFunction = t.callExpression(fooIdentifier, [declaration]); 49 | path.node.declaration = wrappedFunction; 50 | } 51 | } 52 | } 53 | }); 54 | 55 | 56 | export default wrapWithAgentObserver -------------------------------------------------------------------------------- /ast-transforms/agents/agent-transform.js: -------------------------------------------------------------------------------- 1 | import { transformAsync } from '@babel/core'; 2 | import wrapWithAgentObserver from './agent-observability.js'; 3 | import getPromptInfoFromAgent from './agent-prompt-info.js'; 4 | import AgentError from './agentError.js'; 5 | import path from 'path' 6 | import ReasonError from '../../utils/reasonError.js'; 7 | 8 | export default async function agentTransform(code, filePath) { 9 | const state = { 10 | hasFoundAgentCall: false, 11 | hasCreatedActionArray: false, 12 | reason: '', 13 | } 14 | 15 | const extractPromptInfoPlugin = getPromptInfoFromAgent(code, filePath, state) 16 | 17 | const result = await transformAsync(code, { 18 | presets: [ 19 | ['@babel/preset-typescript', { allExtensions: true, isTSX: false }] 20 | ], 21 | plugins: [extractPromptInfoPlugin, wrapWithAgentObserver], 22 | sourceType: 'module', 23 | }); 24 | 25 | if (!state.hasFoundAgentCall) { 26 | throw new ReasonError(`Did not found a \`useAgent()\` call inside the agent ${path.basename(filePath)} in ${filePath}.\n\nThis can happen for a few reasons:\n1) You are not calling the actual function inside your agent — anv every agent needs to call it;\n2) You renamed the fuction when you imported — e.g. \`import { useAgent as anotherName }\` — which is not allowed at the moment.`, 38) 27 | } 28 | 29 | return result.code 30 | } -------------------------------------------------------------------------------- /ast-transforms/agents/agentError.js: -------------------------------------------------------------------------------- 1 | import isDebug from "../../utils/isDebug.js"; 2 | 3 | export default class AgentError extends Error { 4 | name; 5 | description; 6 | code; 7 | 8 | constructor(description, code, debug_info = null) { 9 | const message = `AgentError: ${description}\nError code: ${code}` 10 | super(message) 11 | 12 | this.name = 'AgentError' 13 | this.description = description 14 | this.code = code 15 | 16 | if (isDebug) { 17 | console.log('RΞASON — INTERNAL DEBUG INFORMATION:'); 18 | console.log(debug_info); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /ast-transforms/agents/agentObserver.ts: -------------------------------------------------------------------------------- 1 | import { context as otelContext } from '@opentelemetry/api' 2 | import IContext from "../../observability/context" 3 | import { Trace } from "../../observability/tracer" 4 | import asyncLocalStorage from "../../utils/asyncLocalStorage" 5 | 6 | export default function agentObserver(fn: Function) { 7 | return async function (...args: any[]) { 8 | const context = asyncLocalStorage.getStore() as IContext 9 | 10 | const trace = new Trace(context, `[AGENT] ${fn.name}`, 'agent') 11 | return trace.startActiveSpan(async (span: any) => { 12 | trace.addAttribute('agent.name', fn.name) 13 | trace.addAttribute('agent.input', args) 14 | 15 | let result 16 | const currentOtelContext = otelContext.active() 17 | const newOtelContext = currentOtelContext.setValue(Symbol('trace'), trace).setValue(Symbol('otel-context'), currentOtelContext) 18 | await otelContext.with(newOtelContext, async () => { 19 | try { 20 | result = await fn(...args) 21 | } catch (err) { 22 | trace.err(err) 23 | throw err 24 | } 25 | }) 26 | 27 | trace.addAttribute('agent.output', result) 28 | trace.end() 29 | return result 30 | }) 31 | } 32 | } 33 | 34 | // asyncStores[actionId] = asyncStore 35 | 36 | // i could create a new store with a newly generated actionId 37 | // but how would i be able to get that id in child actions? 38 | // asyncStores have the nice property of literally only existing in the current execution context -------------------------------------------------------------------------------- /ast-transforms/getBasePath.js: -------------------------------------------------------------------------------- 1 | import { libname } from "./isPathFromReason.js"; 2 | 3 | // windows fucking sucks 3 4 | function isWindows() { 5 | return process.platform === 'win32' 6 | } 7 | 8 | export default function getBasePath() { 9 | let reasonEnv = process.env?.REASON_ENV 10 | // honestly window fucking sucks 11 | if (isWindows() && reasonEnv) { 12 | reasonEnv = reasonEnv.trim() 13 | } 14 | if (reasonEnv === 'dev') { 15 | let path = `${process.cwd()}/node_modules/${libname}/dist/ast-transforms/`; 16 | 17 | if (isWindows()) { 18 | path = 'file:///' + path 19 | } 20 | 21 | return path 22 | } 23 | // now we now REASON is running in dev mode (MY MACHINE) 24 | return `${process.cwd()}/ast-transforms/`; 25 | } 26 | -------------------------------------------------------------------------------- /ast-transforms/internalname.js: -------------------------------------------------------------------------------- 1 | const internalname = '__internal_DO_NOT_USE_' 2 | export default internalname -------------------------------------------------------------------------------- /ast-transforms/isPathFromReason.js: -------------------------------------------------------------------------------- 1 | import { libname } from "../utils/libname.js" 2 | 3 | export { libname } 4 | 5 | export default function isPathFromReason(p) { 6 | if (process.env?.REASON_ENV === 'local') { 7 | if (p.includes('functions')) { 8 | return true 9 | } 10 | 11 | if (p.includes('ast-transforms')) { 12 | return true 13 | } 14 | 15 | return false 16 | } 17 | 18 | const fromLib = p.includes(libname) 19 | return fromLib 20 | } -------------------------------------------------------------------------------- /ast-transforms/think/think-prompt-info.js: -------------------------------------------------------------------------------- 1 | import ts2ast from '../utils/to-ts-ast.js' 2 | import isPathFromReason from '../isPathFromReason.js' 3 | import getTsTypes from '../utils/get-ts-types.js' 4 | import ReasonError from '../../utils/reasonError.js' 5 | import internalname from '../internalname.js'; 6 | import { toJsonSchema } from '../utils/to-json-schema.js' 7 | import parseJSDoc from '../utils/jsdoc-parser.js' 8 | 9 | function isDefaultSpecifier(specifier) { 10 | if (specifier.type === 'ImportDefaultSpecifier') return true 11 | if (specifier.imported.name === 'default') return true 12 | 13 | return false 14 | } 15 | 16 | // interface ExtractorInfo { 17 | // name: string; 18 | // type: string; 19 | // prompt?: string; 20 | // } 21 | 22 | export default function getPromptInfoFromThink(code, filePath, isJs = false) { 23 | const codetypes = new Map(); 24 | let imports 25 | 26 | let thinkname 27 | 28 | if (!isJs) { 29 | const ast = ts2ast(code) 30 | imports = getTsTypes(ast, codetypes, filePath, isJs) 31 | } 32 | 33 | function extractJsDocPlugin({ types: t }) { 34 | return { 35 | visitor: { 36 | ImportDeclaration(path) { 37 | for (let specifier of path.node.specifiers) { 38 | let i = { 39 | varname: specifier.local.name, 40 | orginalname: specifier.imported?.name ?? specifier.local.name, 41 | default: isDefaultSpecifier(specifier), 42 | node: path.node 43 | } 44 | 45 | if (isPathFromReason(path.node.source.value)) { 46 | if (i.orginalname === 'reason') { 47 | specifier.imported.name = internalname + 'reason'; 48 | 49 | path.scope.rename(internalname + 'reason', i.varname); 50 | thinkname = i.varname; 51 | } 52 | } 53 | } 54 | }, 55 | CallExpression(path) { 56 | if (t.isIdentifier(path.node.callee, { name: thinkname }) && (path.node.arguments.length === 1 || path.node.arguments.length === 2)) { 57 | // if there is no in the `think()` function, we just ignore & move on 58 | if (!path.node.typeParameters) return 59 | 60 | if (path.node.typeParameters.params.length !== 1) { 61 | throw new ReasonError(`The \`think\` function can only receive one generic.`, 53) 62 | } 63 | 64 | const param = path.node.typeParameters.params[0] 65 | 66 | let annotation = param 67 | 68 | if (param.type === 'TSTypeReference') { 69 | // this means that the type is a type alias (to a `type` or an `interface`) 70 | const name = param.typeName.name 71 | if (!codetypes.has(name) && !imports.has(name)) { 72 | throw new ReasonError(`Could not find type \`${name}\` of parameter \`${params[i].name}\`. All parameters need to have a defined type.`, 23); 73 | } 74 | 75 | if (!codetypes.has(name)) { 76 | throw new ReasonError(`You are trying to use the interface/type \`${name}\` as the generic for the \`reason()\` call. But, sadly, RΞASON does not support importing types from other files yet. Please define the type in this file.\n\nThis will be coming in our next release.`, 554) 77 | } 78 | 79 | const type = codetypes.get(name) 80 | if (type.type !== "TSInterfaceDeclaration" && type.type !== "TSTypeAliasDeclaration") { 81 | throw new ReasonError(`You are trying to use the interface/type \`${name}\` as the generic for the \`reason()\` call. But, sadly, RΞASON does not support importing types from other files yet. Please define the type in this file.\n\nThis will be coming in our next release.`,555) 82 | } 83 | 84 | if (type.extends) { 85 | throw new ReasonError('We do not support extending types/interfaces as it does not work well with LLMs. Please remove the `extends` keyword from your type/interface.', 525) 86 | } 87 | 88 | if (type.type === "TSInterfaceDeclaration") { 89 | annotation = type.body 90 | annotation.type = 'TSTypeLiteral' 91 | annotation.members = annotation.body 92 | } 93 | else { 94 | // `type a = 'abc` 95 | annotation = type.typeAnnotation 96 | } 97 | } 98 | 99 | let schemaType = toJsonSchema(annotation) 100 | let prompts = [] 101 | 102 | for (let member of annotation.members) { 103 | let desc = member.leadingComments?.[0]?.value ?? '' 104 | 105 | if (desc) { 106 | let jdoc = parseJSDoc(desc) 107 | desc = jdoc.description 108 | } 109 | 110 | let p = { 111 | name: member.key.name, 112 | type: JSON.stringify(schemaType.properties[member.key.name]), 113 | prompt: desc ?? null, 114 | required: schemaType.required.includes(member.key.name) 115 | } 116 | 117 | prompts.push(p) 118 | } 119 | 120 | let items = [] 121 | 122 | for (let prompt of prompts) { 123 | items.push(t.objectExpression([ 124 | t.objectProperty(t.identifier('name'), t.stringLiteral(prompt.name)), 125 | t.objectProperty(t.identifier('type'), t.stringLiteral(prompt.type)), 126 | t.objectProperty(t.identifier('prompt'), t.stringLiteral(prompt.prompt)), 127 | t.objectProperty(t.identifier('required'), t.booleanLiteral(prompt.required)), 128 | ])) 129 | } 130 | 131 | const arrayOfObjects = t.arrayExpression(items); 132 | 133 | if (path.node.arguments.length === 1) { 134 | path.node.arguments.push(t.nullLiteral()) 135 | } 136 | 137 | path.node.arguments.push(arrayOfObjects) 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | return extractJsDocPlugin 145 | } -------------------------------------------------------------------------------- /ast-transforms/think/think-stream-prompt-info.js: -------------------------------------------------------------------------------- 1 | import ts2ast from '../utils/to-ts-ast.js' 2 | import isPathFromReason from '../isPathFromReason.js' 3 | import getTsTypes from '../utils/get-ts-types.js' 4 | import ReasonError from '../../utils/reasonError.js' 5 | import internalname from '../internalname.js'; 6 | import { toJsonSchema } from '../utils/to-json-schema.js' 7 | import parseJSDoc from '../utils/jsdoc-parser.js' 8 | 9 | function isDefaultSpecifier(specifier) { 10 | if (specifier.type === 'ImportDefaultSpecifier') return true 11 | if (specifier.imported.name === 'default') return true 12 | 13 | return false 14 | } 15 | 16 | // interface ExtractorInfo { 17 | // name: string; 18 | // type: string; 19 | // prompt?: string; 20 | // } 21 | 22 | export default function getPromptInfoFromThinkStream(code, filePath, isJs = false) { 23 | const codetypes = new Map(); 24 | let imports 25 | 26 | let thinkname 27 | 28 | if (!isJs) { 29 | const ast = ts2ast(code) 30 | imports = getTsTypes(ast, codetypes, filePath, isJs) 31 | } 32 | 33 | function extractJsDocPlugin({ types: t }) { 34 | return { 35 | visitor: { 36 | ImportDeclaration(path) { 37 | for (let specifier of path.node.specifiers) { 38 | let i = { 39 | varname: specifier.local.name, 40 | orginalname: specifier.imported?.name ?? specifier.local.name, 41 | default: isDefaultSpecifier(specifier), 42 | node: path.node 43 | } 44 | 45 | if (isPathFromReason(path.node.source.value)) { 46 | if (i.orginalname === 'reasonStream') { 47 | specifier.imported.name = internalname + 'reasonStream'; 48 | 49 | path.scope.rename(internalname + 'reasonStream', i.varname); 50 | thinkname = i.varname; 51 | } 52 | } 53 | } 54 | }, 55 | CallExpression(path) { 56 | if (t.isIdentifier(path.node.callee, { name: thinkname }) && (path.node.arguments.length === 1 || path.node.arguments.length === 2)) { 57 | // if there is no in the `think()` function, we just ignore & move on 58 | if (!path.node.typeParameters) return 59 | 60 | if (path.node.typeParameters.params.length !== 1) { 61 | throw new ReasonError(`The \`reasonStream\` function can only receive one generic.`, 53) 62 | } 63 | 64 | const param = path.node.typeParameters.params[0] 65 | 66 | let annotation = param 67 | 68 | if (param.type === 'TSTypeReference') { 69 | // this means that the type is a type alias (to a `type` or an `interface`) 70 | const name = param.typeName.name 71 | if (!codetypes.has(name) && !imports.has(name)) { 72 | throw new ReasonError(`Could not find type \`${name}\` of parameter \`${params[i].name}\`. All parameters need to have a defined type.`, 23); 73 | } 74 | 75 | if (!codetypes.has(name)) { 76 | throw new ReasonError(`You are trying to use the interface/type \`${name}\` as the generic for the \`reasonStream()\` call. But, sadly, RΞASON does not support importing types from other files yet. Please define the type in this file.\n\nThis will be coming in our next release.`, 554) 77 | } 78 | 79 | const type = codetypes.get(name) 80 | if (type.type !== "TSInterfaceDeclaration" && type.type !== "TSTypeAliasDeclaration") { 81 | throw new ReasonError(`You are trying to use the interface/type \`${name}\` as the generic for the \`reasonStream()\` call. But, sadly, RΞASON does not support importing types from other files yet. Please define the type in this file.\n\nThis will be coming in our next release.`, 555) 82 | } 83 | 84 | if (type.extends) { 85 | throw new ReasonError('We do not support extending types/interfaces as it does not work well with LLMs. Please remove the `extends` keyword from your type/interface.', 525) 86 | } 87 | 88 | if (type.type === "TSInterfaceDeclaration") { 89 | annotation = type.body 90 | annotation.type = 'TSTypeLiteral' 91 | annotation.members = annotation.body 92 | } 93 | else { 94 | // `type a = 'abc` 95 | annotation = type.typeAnnotation 96 | } 97 | } 98 | 99 | let schemaType = toJsonSchema(annotation) 100 | let prompts = [] 101 | 102 | for (let member of annotation.members) { 103 | let desc = member.leadingComments?.[0]?.value ?? '' 104 | 105 | if (desc) { 106 | let jdoc = parseJSDoc(desc) 107 | desc = jdoc.description 108 | } 109 | 110 | let p = { 111 | name: member.key.name, 112 | type: JSON.stringify(schemaType.properties[member.key.name]), 113 | prompt: desc ?? null, 114 | required: schemaType.required.includes(member.key.name) 115 | } 116 | 117 | prompts.push(p) 118 | } 119 | 120 | let items = [] 121 | 122 | for (let prompt of prompts) { 123 | items.push(t.objectExpression([ 124 | t.objectProperty(t.identifier('name'), t.stringLiteral(prompt.name)), 125 | t.objectProperty(t.identifier('type'), t.stringLiteral(prompt.type)), 126 | t.objectProperty(t.identifier('prompt'), t.stringLiteral(prompt.prompt)), 127 | t.objectProperty(t.identifier('required'), t.booleanLiteral(prompt.required)), 128 | ])) 129 | } 130 | 131 | const arrayOfObjects = t.arrayExpression(items); 132 | 133 | if (path.node.arguments.length === 1) { 134 | path.node.arguments.push(t.nullLiteral()) 135 | } 136 | 137 | path.node.arguments.push(arrayOfObjects) 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | return extractJsDocPlugin 145 | } -------------------------------------------------------------------------------- /ast-transforms/think/think-transform.js: -------------------------------------------------------------------------------- 1 | import { transformAsync } from '@babel/core'; 2 | import getPromptInfoFromThink from './think-prompt-info.js'; 3 | import getPromptInfoFromThinkStream from '../think/think-stream-prompt-info.js' 4 | import isJs from '../utils/isJs.js'; 5 | import isPathFromReason from '../isPathFromReason.js'; 6 | 7 | export default async function thinkTransform(code, filePath) { 8 | const extractPromptInfoPlugin = getPromptInfoFromThink(code, filePath, isJs(filePath)) 9 | 10 | const plugins = [extractPromptInfoPlugin] 11 | 12 | if (isPathFromReason(code) && code.includes('reasonStream')) { 13 | // can potentially use `think` 14 | const thinkPlugin = getPromptInfoFromThinkStream(code, filePath, isJs(filePath)) 15 | plugins.push(thinkPlugin) 16 | } 17 | 18 | const result = await transformAsync(code, { 19 | presets: [ 20 | ['@babel/preset-typescript', { allExtensions: true, isTSX: false }] 21 | ], 22 | plugins: plugins, 23 | sourceType: 'module', 24 | }); 25 | 26 | return result.code 27 | } -------------------------------------------------------------------------------- /ast-transforms/think/thinkError.js: -------------------------------------------------------------------------------- 1 | import isDebug from "../../utils/isDebug.js"; 2 | 3 | export default class ThinkError extends Error { 4 | name; 5 | description; 6 | code; 7 | 8 | constructor(description, code, debug_info = {}) { 9 | const message = `ThinkError: ${description}\nError code: ${code}` 10 | super(message) 11 | 12 | this.name = 'ThinkError' 13 | this.description = description 14 | this.code = code 15 | 16 | if (isDebug) { 17 | console.log('RΞASON — INTERNAL DEBUG INFORMATION:'); 18 | console.log(debug_info); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /ast-transforms/utils/get-imports.js: -------------------------------------------------------------------------------- 1 | import { default as traverse } from '@babel/traverse' 2 | import { default as nodepath } from 'path' 3 | import fs from 'fs' 4 | 5 | /* 6 | { 7 | localfilename: nodepath.basename(filePath), 8 | localname: specifier.local.name, 9 | originalnme: specifier.imported.name, 10 | path: ipath, 11 | filepath: filePath, 12 | fullpath: nodepath.join(filePath, back, ipath), 13 | default: isDefaultSpecifier(specifier), 14 | } 15 | */ 16 | 17 | function isDefaultSpecifier(specifier) { 18 | if (specifier.type === 'ImportDefaultSpecifier') return true 19 | if (specifier.imported.name === 'default') return true 20 | 21 | return false 22 | } 23 | 24 | export default function getImports(ast, imports, filePath, isJs) { 25 | traverse.default(ast, { 26 | ImportDeclaration(path) { 27 | let ipath = path.node.source.value; 28 | 29 | let back = '' 30 | if (ipath.startsWith('../')) { 31 | back = '..' 32 | } 33 | 34 | for (let specifier of path.node.specifiers) { 35 | let i = { 36 | localfilename: nodepath.basename(filePath), 37 | localname: specifier.local.name, 38 | originalnme: specifier.imported?.name ?? specifier.local.name, 39 | path: ipath, 40 | filepath: filePath, 41 | fullpath: nodepath.join(filePath, back, ipath), 42 | default: isDefaultSpecifier(specifier), 43 | } 44 | 45 | imports.set(i.localname, i) 46 | } 47 | 48 | let i = { 49 | name: '', 50 | path: '', 51 | default: false, 52 | } 53 | } 54 | }) 55 | } 56 | 57 | export function import2file(i) { 58 | let ext = nodepath.extname(i.fullpath) 59 | 60 | if (Boolean(ext) === false) { 61 | ext = nodepath.extname(i.localfilename) 62 | } 63 | 64 | if (!fs.existsSync(i.fullpath + ext)) { 65 | throw new Error(`File not found: ${i.fullpath + ext}`) 66 | } 67 | 68 | return fs.readFileSync(i.fullpath + ext, 'utf8') 69 | } -------------------------------------------------------------------------------- /ast-transforms/utils/get-ts-types.js: -------------------------------------------------------------------------------- 1 | import getImports, { import2file } from './get-imports.js' 2 | import { default as traverse } from '@babel/traverse' 3 | 4 | export default function getTsTypes(ast, typesMap, filePath, isJs) { 5 | getTypesInFile(ast, typesMap) 6 | 7 | const imports = new Map() 8 | getImports(ast, imports, filePath, isJs) 9 | 10 | return imports 11 | } 12 | 13 | function getTypesInFile(ast, map) { 14 | traverse.default(ast, { 15 | TSTypeAliasDeclaration(path) { 16 | map.set(path.node.id.name, path.node) 17 | }, 18 | TSInterfaceDeclaration(path) { 19 | map.set(path.node.id.name, path.node) 20 | }, 21 | }) 22 | } -------------------------------------------------------------------------------- /ast-transforms/utils/isJs.js: -------------------------------------------------------------------------------- 1 | export default function isJs(filepath) { 2 | return filepath.endsWith('.js') || filepath.endsWith('.mjs') 3 | } -------------------------------------------------------------------------------- /ast-transforms/utils/jsdoc-parser.js: -------------------------------------------------------------------------------- 1 | function initialParse(str) { 2 | let lines = str.split('\n') 3 | 4 | let description = '' 5 | let descdone = false 6 | let parameters = [] 7 | 8 | for (let line of lines) { 9 | line = line.replace('*', '') 10 | if (line.trim().startsWith('@param')) { 11 | descdone = true 12 | parameters.push('') 13 | } 14 | 15 | if (descdone) { 16 | parameters[parameters.length - 1] += line + '\n' 17 | } 18 | 19 | if (!descdone) { 20 | description += line.trim() + '\n' 21 | } 22 | } 23 | 24 | description = description.trim() 25 | for (let i = 0; i < parameters.length; i++) parameters[i] = parameters[i].trim() 26 | 27 | return { description, parameters} 28 | } 29 | 30 | function parseParameter(parameter) { 31 | let parsed = { 32 | name: '', 33 | required: true, 34 | } 35 | 36 | parameter = parameter.replace('@param ', '').trim() 37 | 38 | if (parameter.startsWith('{')) { 39 | let type = parameter.split('}')[0].replace('{', '').trim() 40 | parameter = parameter.replace(`{${type}}`, '').trim() 41 | parsed.type = type 42 | 43 | if (type.endsWith('?')) { 44 | parsed.required = false 45 | parsed.type = type.replace('?', '') 46 | } 47 | } 48 | 49 | if (parameter.startsWith('[')) { 50 | parsed.required = false 51 | let name = parameter.split(']')[0].replace('[', '').trim() 52 | parameter = parameter.replace(`[${name}]`, '').trim() 53 | parsed.name = name 54 | } else { 55 | let space = parameter.indexOf(' ') 56 | if (space === -1) { 57 | // if theres no description, the name is the whole thing 58 | parsed.name = parameter 59 | parsed.description = '' 60 | return parsed 61 | } 62 | let name = parameter.substring(0, space) 63 | parsed.name = name 64 | parameter = parameter.substring(space).trim() 65 | } 66 | 67 | if (parameter.length > 0) { 68 | if (parameter.startsWith('-')) { 69 | parameter.replace('-', '').trim() 70 | } 71 | 72 | parsed.description = parameter 73 | } 74 | 75 | return parsed 76 | } 77 | 78 | function finalParse(tree) { 79 | let jsdoc = { 80 | description: tree.description, 81 | parameters: [] 82 | } 83 | 84 | for (let parameter of tree.parameters) { 85 | jsdoc.parameters.push(parseParameter(parameter)) 86 | } 87 | 88 | return jsdoc 89 | } 90 | 91 | export default function parseJSDoc(str) { 92 | return finalParse(initialParse(str)) 93 | } -------------------------------------------------------------------------------- /ast-transforms/utils/to-json-schema.js: -------------------------------------------------------------------------------- 1 | import ReasonError from '../../utils/reasonError.js' 2 | import ts2ast from './to-ts-ast.js' 3 | import parseJSDoc from './jsdoc-parser.js' 4 | 5 | 6 | const NOT_ALLOWED_TYPES = [ 7 | 'TSAnyKeyword', 8 | 'TSUnknownKeyword', 9 | 'TSVoidKeyword', 10 | 'TSNullKeyword', 11 | 'TSNeverKeyword', 12 | 'TSUndefinedKeyword', 13 | 'TSThisType', 14 | ] 15 | 16 | const astTypes2tsTypes = { 17 | 'TSAnyKeyword': 'any', 18 | 'TSUnknownKeyword': 'unknown', 19 | 'TSVoidKeyword': 'void', 20 | 'TSNullKeyword': 'null', 21 | 'TSNeverKeyword': 'never', 22 | 'TSUndefinedKeyword': 'undefined', 23 | 'TSThisType': 'this', 24 | 25 | 'TSStringKeyword': 'string', 26 | 'TSNumberKeyword': 'number', 27 | 'TSBooleanKeyword': 'boolean', 28 | } 29 | 30 | function getDesc(annotation) { 31 | let desc = annotation.leadingComments?.[0]?.value ?? ''; 32 | if (desc) { 33 | let jdoc = parseJSDoc(desc); 34 | desc = jdoc.description; 35 | return { description: desc } 36 | } 37 | return {} 38 | } 39 | 40 | function handleStringType(annotation) { 41 | return { 42 | type: 'string' 43 | } 44 | } 45 | 46 | function handleNumberType(annotation) { 47 | return { 48 | type: 'number' 49 | } 50 | } 51 | 52 | function handleBooleanType(annotation) { 53 | return { 54 | type: 'boolean' 55 | } 56 | } 57 | 58 | function handleUnionType(annotation) { 59 | const r = { 60 | enum: [] 61 | } 62 | 63 | for (let type of annotation.types) { 64 | if (type.type !== 'TSLiteralType') { 65 | // TODO: link doc 66 | throw new ReasonError('You can only use literal types in a TypeScript union.\n\nFor example `type union = "foo" | "bar" | 10` is valid.\nBut `type union = string | number` is not.\n\nFor more information see link doc', 9) 67 | } 68 | 69 | r.enum.push(type.literal.value) 70 | } 71 | 72 | return r 73 | } 74 | 75 | function handleArrayType(annotation) { 76 | let r = { 77 | type: 'array', 78 | items: { 79 | 80 | } 81 | } 82 | 83 | if (!annotation.elementType?.type) { 84 | throw new ReasonError(`element type is not an object: ${annotation}`, 13) 85 | } 86 | 87 | r.items = toJsonSchema(annotation.elementType) 88 | return r 89 | } 90 | 91 | function handleParatensisType(annotation) { 92 | return toJsonSchema(annotation.typeAnnotation) 93 | } 94 | 95 | function handleObjectType(annotation) { 96 | const r = { 97 | type: 'object', 98 | properties: {}, 99 | required: [] 100 | } 101 | 102 | for (let member of annotation.members) { 103 | let m = {} 104 | 105 | if (member.type !== 'TSPropertySignature') { 106 | throw new ReasonError(`Member type ${member.type} not yet supported.`, 15) 107 | } 108 | 109 | if (!member.optional) { 110 | r.required.push(member.key.name) 111 | } 112 | const memberSchema = { 113 | ...getDesc(member), 114 | ...toJsonSchema(member.typeAnnotation.typeAnnotation) 115 | } 116 | r.properties[member.key.name] = memberSchema 117 | } 118 | 119 | return r 120 | } 121 | 122 | function handleTupleType(annotation) { 123 | const r = { 124 | type: 'array', 125 | prefixItems: [ 126 | 127 | ] 128 | } 129 | 130 | for (let type of annotation.elementTypes) { 131 | r.prefixItems.push(toJsonSchema(type)) 132 | } 133 | 134 | return r 135 | } 136 | 137 | export function toJsonSchema(annotation) { 138 | const schemaType = {} 139 | 140 | if (NOT_ALLOWED_TYPES.includes(annotation.type)) { 141 | // TODO: link doc 142 | throw new ReasonError(`You cannot use ${astTypes2tsTypes[annotation.type]} type as a LLM parameter. Check our documentation for more information.`, 7) 143 | } 144 | 145 | switch(annotation.type) { 146 | case 'TSStringKeyword': { 147 | return handleStringType(annotation) 148 | } 149 | 150 | case 'TSNumberKeyword': { 151 | return handleNumberType(annotation) 152 | } 153 | 154 | case 'TSBooleanKeyword': { 155 | return handleBooleanType(annotation) 156 | } 157 | 158 | case 'TSUnionType': { 159 | return handleUnionType(annotation) 160 | } 161 | 162 | case 'TSArrayType': { 163 | return handleArrayType(annotation) 164 | } 165 | 166 | case 'TSTypeLiteral': { 167 | return handleObjectType(annotation) 168 | } 169 | 170 | case 'TSParenthesizedType': { 171 | return handleParatensisType(annotation) 172 | } 173 | 174 | case 'TSTupleType': { 175 | return handleTupleType(annotation) 176 | } 177 | 178 | case 'TSLiteralType': { 179 | throw new ReasonError('Literal type are disallowed as it doesn\'t make sense to use them in a LLM call.\nA literal type is `type literal = "foo"`, `type literal = 17`, etc.\n\nLiteral types are only allowed in union types — e.g. `type Foo = "foo" | "bar"`', 16) 180 | } 181 | 182 | // TODO: add support 183 | case 'TSTypeReference': { 184 | throw new ReasonError('Sadly, we currently do not support utility types — such as `Omit<>`, `Partial<>`, etc. We do plan on adding support on our next release.', 17) 185 | } 186 | 187 | default: { 188 | throw new ReasonError(`Type ${annotation.type} not yet supported.`, 8) 189 | } 190 | } 191 | } 192 | 193 | export function jdoc2jsonSchema(jdoctype) { 194 | const code = `function inacio(mattos: ${jdoctype}) {}` 195 | const ast = ts2ast(code) 196 | 197 | const typeAnnotation = ast.program.body[0].params[0].typeAnnotation.typeAnnotation 198 | 199 | return toJsonSchema(typeAnnotation) 200 | } 201 | -------------------------------------------------------------------------------- /ast-transforms/utils/to-ts-ast.js: -------------------------------------------------------------------------------- 1 | import parser from '@babel/parser' 2 | 3 | export default function ts2ast(code) { 4 | const ast = parser.parse(code, { 5 | sourceType: 'module', 6 | plugins: ['typescript'], 7 | }); 8 | 9 | return ast 10 | } -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/try-reason/reason/113046a45d8cceea6ac6cfe87af77565fdabf06a/bun.lockb -------------------------------------------------------------------------------- /commands/reason-command.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {spawn} from 'child_process'; 3 | import { fileURLToPath } from 'url'; 4 | import { dirname, join, resolve, sep } from 'path'; 5 | import open from 'open' 6 | import fs from 'fs' 7 | import c from 'ansi-colors' 8 | 9 | const semver = process.version; 10 | const [major, minor, patch] = semver.slice(1).split('.').map(Number); 11 | 12 | if (major < 18) { 13 | console.error('RΞASON requires Node.js v18 or higher.'); 14 | process.exit(1); 15 | } 16 | 17 | const currentDirectory = dirname(fileURLToPath(import.meta.url)); 18 | const distPath = join(currentDirectory, '..', 'dist'); 19 | const webPath = join(currentDirectory, '..', 'web'); 20 | 21 | let loaderPath = join(distPath, 'compiler', 'ts-dev-mode-loader.js') 22 | if (isWindows()) { 23 | loaderPath = 'file:///' + loaderPath 24 | } 25 | 26 | let serverPath = join(distPath, 'server', 'index.js') 27 | if (isWindows()) { 28 | serverPath = 'file:///' + serverPath 29 | } 30 | 31 | const args = process.argv.slice(2); 32 | 33 | if (args.length === 0) { 34 | console.error('Please provide a command.'); 35 | process.exit(1); 36 | } 37 | let command; 38 | let webCommand 39 | 40 | 41 | function isWindows() { 42 | return process.platform === 'win32' 43 | } 44 | 45 | function exportEnv(key, value) { 46 | return isWindows() ? `set ${key}=${value}` : `export ${key}="${value}"` 47 | } 48 | 49 | let port = 1704 50 | let webPort = 1710 51 | let shouldRunWeb = true 52 | function setup() { 53 | if (args.findIndex(arg => arg === '--no-playground') !== -1) { 54 | shouldRunWeb = false 55 | } 56 | 57 | if (args.findIndex(arg => arg === '--playground-port') !== -1) { 58 | try { 59 | webPort = parseInt(args[args.findIndex(arg => arg === '--playground-port') + 1]) 60 | } catch { 61 | console.error('Invalid port number for the Web Playground:', args[args.findIndex(arg => arg === '--playground-port') + 1]) 62 | console.error('Will be using default port:', webPort) 63 | } 64 | } 65 | 66 | if (args.findIndex(arg => arg === '--port') !== -1) { 67 | try { 68 | port = parseInt(args[args.findIndex(arg => arg === '--port') + 1]) 69 | } catch { 70 | console.error('Invalid port number:', args[args.findIndex(arg => arg === '--port') + 1]) 71 | console.error('Will be using default port:', port) 72 | } 73 | } 74 | } 75 | 76 | function getStartupFile() { 77 | if (fs.existsSync('custom-setup.js')) return 'custom-setup.js' 78 | if (fs.existsSync('custom-setup.ts')) return 'custom-setup.ts' 79 | return null 80 | } 81 | 82 | async function sleep(ms) { 83 | return new Promise(resolve => setTimeout(resolve, ms)); 84 | } 85 | 86 | setup() 87 | 88 | let isWebReady = false 89 | let isServerReady = false 90 | function openWeb() { 91 | if (isWebReady && isServerReady) { 92 | open(`http://localhost:${webPort}`) 93 | } 94 | } 95 | 96 | switch (args[0]) { 97 | case 'dev': 98 | command = spawn(`${exportEnv('REASON_ENV', "dev")} && nodemon --quiet -e js,ts --ignore node_modules --watch . --no-warnings --loader ${loaderPath} ${serverPath} --port ${port}`, { shell: true, stdio: 'pipe' }); 99 | command.stdout.on('data', async (data) => { 100 | process.stdout.write(data); 101 | if (data.includes(' Server is ready at port ')) { 102 | isServerReady = true 103 | openWeb() 104 | } 105 | }) 106 | 107 | command.stderr.on('data', (data) => { 108 | process.stderr.write(data); 109 | }) 110 | 111 | let startupFile = getStartupFile() 112 | if (startupFile) { 113 | const startup = spawn(`${exportEnv('REASON_ENV', "dev")} && nodemon -e js,ts --ignore node_modules --watch . --no-warnings --loader ${loaderPath} ${startupFile}`, { shell: true, stdio: 'inherit' }); 114 | startup.on('spawn', () => { 115 | console.log(`RΞASON — Running ${startupFile}...`) 116 | }) 117 | startup.on('close', (code) => { 118 | console.log(`child process exited with code ${code}`); 119 | }); 120 | } 121 | 122 | if (!shouldRunWeb) break; 123 | 124 | webCommand = spawn(`cd ${webPath} && npx next start -p ${webPort}`, { shell: true, stdio: 'pipe' }) 125 | webCommand.stderr.on('data', (data) => { 126 | data = data.toString() 127 | if (data.includes('ExperimentalWarning')) return 128 | console.log('[!] An error has happened in RΞASON Playground.\nError:', data); 129 | }) 130 | 131 | webCommand.stdout.on('data', async (data) => { 132 | data = data.toString() 133 | if (data.includes('✓ Ready')) { 134 | isWebReady = true 135 | console.log(`${c.gray.bold('RΞASON Playground')} — Started at http://localhost:${webPort}`) 136 | openWeb() 137 | } 138 | }) 139 | break; 140 | 141 | case 'dev:debug': 142 | command = spawn(`${exportEnv('REASON_INTERNAL_DEBUG', 'true')} && ${exportEnv('REASON_ENV', "dev")} && nodemon -e js,ts --ignore node_modules --watch . --no-warnings --loader ${loaderPath} ${serverPath} --port ${port}`, { shell: true, stdio: 'inherit' }); 143 | 144 | let startupFie = getStartupFile() 145 | if (startupFie) { 146 | const startup = spawn(`${exportEnv('REASON_INTERNAL_DEBUG', 'true')} && ${exportEnv('REASON_ENV', "dev")} && nodemon -e js,ts --ignore node_modules --watch src --no-warnings --loader ${loaderPath} ${startupFie}`, { shell: true, stdio: 'inherit' }); 147 | startup.on('spawn', () => { 148 | console.log(`RΞASON — Running ${startupFie}...`) 149 | }) 150 | startup.on('close', (code) => { 151 | console.log(`RΞASON — \`${startupFie}\` process exited with code ${code}`); 152 | }); 153 | } 154 | 155 | if (!shouldRunWeb) break; 156 | 157 | webCommand = spawn(`cd ${webPath} && npx next start -p ${webPort}`, { shell: true, stdio: 'pipe' }) 158 | webCommand.stdout.on('data', async (data) => { 159 | process.stdout.write(data); 160 | if (data.includes('✓ Ready')) { 161 | await sleep(3_000) 162 | console.log(`RΞASON Playground — Started at http://localhost:${webPort}`) 163 | } 164 | }) 165 | 166 | webCommand.stderr.on('data', (data) => { 167 | data = data.toString() 168 | console.log('[!] An error has happened in RΞASON Playground.\nError:', data); 169 | }) 170 | break; 171 | 172 | default: 173 | console.error('Unknown command:', args[0]); 174 | process.exit(1); 175 | } 176 | -------------------------------------------------------------------------------- /commands/tryreason-command.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {exec, execSync, spawn} from 'child_process'; 3 | import { fileURLToPath } from 'url'; 4 | import { dirname, join, sep } from 'path'; 5 | import fse from 'fs-extra' 6 | import fs from 'fs' 7 | import c from 'ansi-colors' 8 | import enq from 'enquirer' 9 | 10 | console.log(c.gray.italic.bold(` 11 | WELCOME TO 12 | ______ _______ _______ _______ _______ _______ 13 | | __ \\ _____ | _ | __| | | | 14 | | < _____ | |__ | - | | 15 | |___|__|_______|___|___|_______|_______|__|____| 16 | 17 | THE MINIMALISTIC LLM FRAMEWORK 18 | `)); 19 | 20 | const commandDir = dirname(fileURLToPath(import.meta.url)); 21 | 22 | let projectDir = null 23 | let isDebug = false 24 | if (process.argv.indexOf('--debug') !== -1) isDebug = true 25 | else if (process.argv.length >= 3) projectDir = process.argv[2] 26 | 27 | 28 | const options = { 29 | name: null, 30 | oaiKey: null, 31 | packageManager: null, 32 | } 33 | 34 | async function projectname() { 35 | function isValidString(str) { 36 | const regex = /^[A-Za-z0-9-]+$/; 37 | return regex.test(str); 38 | } 39 | 40 | let prompt = new enq.Input({ 41 | message: 'What is the name of your project?', 42 | initial: 'my-reason-app', 43 | validate: input => { 44 | if (input.trim().length === 0) { 45 | return 'Project name cannot be empty' 46 | } 47 | 48 | if (input.includes(' ')) { 49 | return 'Project name cannot contain spaces' 50 | } 51 | 52 | if (!isValidString(input)) { 53 | return 'Project name can only contain letters, numbers and dashes' 54 | } 55 | 56 | return true 57 | }, 58 | styles: { 59 | success: c.gray.bold, 60 | } 61 | }) 62 | 63 | return prompt.run() 64 | } 65 | 66 | async function oaikey() { 67 | let prompt = new enq.Password({ 68 | message: 'What is the OpenAI API key this project will use? (you can leave empty and add it later in .env file)', 69 | styles: { 70 | success: c.gray.bold, 71 | selection: c.gray.bold, 72 | select: c.gray.bold, 73 | primary: c.gray.bold, 74 | em: c.gray.bold, 75 | }, 76 | }) 77 | const key = await prompt.run() 78 | if (key.trim().length === 0) { 79 | return null 80 | } 81 | return key 82 | } 83 | 84 | async function packagemanager() { 85 | const prompt = new enq.Select({ 86 | message: 'Which package manager to use?', 87 | choices: ['npm', 'pnpm', 'yarn'], 88 | styles: { 89 | primary: c.gray.bold, 90 | success: c.gray.bold, 91 | em: c.cyan.underline, 92 | }, 93 | }) 94 | return prompt.run() 95 | } 96 | 97 | async function main() { 98 | options.name = await projectname() 99 | options.oaiKey = await oaikey() 100 | options.packageManager = await packagemanager() 101 | 102 | console.log() 103 | console.log(c.bold('Setting up your project now...')) 104 | 105 | await setupProject() 106 | process.exit(0) 107 | } 108 | 109 | async function setupProject() { 110 | const sampleDir = join(commandDir, '..', 'sample') 111 | if (!projectDir) projectDir = options.name 112 | const destDir = join(process.cwd(), projectDir) 113 | 114 | let install 115 | switch (options.packageManager) { 116 | case 'yarn': { 117 | install = `yarn` 118 | break 119 | } 120 | default: { 121 | install = `${options.packageManager} install` 122 | break 123 | } 124 | } 125 | 126 | if (isDebug) { 127 | console.log('sampleDir', sampleDir) 128 | console.log('destDir', destDir) 129 | console.log('projectDir', projectDir) 130 | console.time('copying') 131 | } 132 | fse.copySync(sampleDir, projectDir) 133 | if (isDebug) console.timeEnd('copying') 134 | 135 | changeFiles(options, destDir) 136 | 137 | // const command = spawn(`cd ${destDir} && pwd && echo '${install}'`, { shell: true, stdio: 'inherit' }) 138 | // const command = exec(`cd ${destDir} && ${install}`) 139 | const child = spawn(`cd ${destDir} && ${install}`, { shell: true, stdio: 'inherit' }); 140 | 141 | return new Promise((resolve, reject) => { 142 | child.on('close', (code) => { 143 | if (code !== 0) { 144 | console.log(c.red.bold(`\n\nSomething went wrong. Please try again.`)) 145 | process.exit(1) 146 | } 147 | 148 | console.log(c.green.bold(`\n${options.name} setup completed!`)) 149 | console.log((`To run, go to its directory and run ${c.bold(`${options.packageManager} ${options.packageManager === 'yarn' ? '' : 'run '}dev`)}`)) 150 | process.exit(0) 151 | }); 152 | // command.stdout.pipe(process.stdout) 153 | // command.stderr.pipe(process.stderr) 154 | // command.on('exit', (code) => { 155 | // if (code !== 0) { 156 | // console.log(c.red.bold(`\n\nSomething went wrong. Please try again.`)) 157 | // process.exit(0) 158 | // } 159 | 160 | // console.log(c.green.bold(`\n\n${options.name} setup completed!`)) 161 | // console.log((`To run, go to its directory and run ${c.bold(`${options.packageManager} run dev`)}`)) 162 | // process.exit(1) 163 | // }) 164 | }) 165 | } 166 | 167 | function changeFiles({ name, oaiKey }, destDir) { 168 | const configPath = join(destDir, '.reason.config.js') 169 | let configContent = fs.readFileSync(configPath, 'utf8') 170 | if (oaiKey) configContent = configContent.replace('', oaiKey) 171 | configContent = configContent.replace('', name) 172 | fs.writeFileSync(configPath, configContent, 'utf8') 173 | } 174 | 175 | main() 176 | -------------------------------------------------------------------------------- /compiler/ts-dev-mode-loader.mjs: -------------------------------------------------------------------------------- 1 | // loader.mjs 2 | import * as esmLoader from '@swc-node/register/esm'; 3 | import fs from 'fs'; 4 | import { URL, fileURLToPath } from 'url'; 5 | import path from 'path'; 6 | import transformAction from '../ast-transforms/actions/actions.js'; 7 | import agentTransform from '../ast-transforms/agents/agent-transform.js'; 8 | import ReasonError from '../utils/reasonError.js'; 9 | import ActionError from '../ast-transforms/actions/actionError.js'; 10 | import AgentError from '../ast-transforms/agents/agentError.js'; 11 | import isPathFromReason from '../ast-transforms/isPathFromReason.js'; 12 | import thinkTransform from '../ast-transforms/think/think-transform.js'; 13 | import ThinkError from '../ast-transforms/think/thinkError.js'; 14 | 15 | function isWindows() { 16 | return process.platform === 'win32' 17 | } 18 | 19 | const semver = process.version; 20 | const [major, minor, patch] = semver.slice(1).split('.').map(Number); 21 | 22 | if (major < 18) { 23 | console.error('RΞASON requires Node.js v18 or higher.'); 24 | process.exit(1); 25 | } 26 | 27 | const TS_LOADER_DEFAULT_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts']; 28 | export async function load(url, context, defaultLoad) { 29 | if (context.format === 'builtin') { 30 | return defaultLoad(url, context, defaultLoad); 31 | } 32 | let newUrl = url; 33 | if (newUrl.endsWith('.mjs') && TS_LOADER_DEFAULT_EXTENSIONS.some((ext) => newUrl.indexOf(ext) >= 0)) { 34 | newUrl = newUrl.replace('.mjs', ''); 35 | } 36 | const filePath = fileURLToPath(new URL(newUrl)); 37 | const code = await fs.promises.readFile(filePath, 'utf-8'); 38 | if (!newUrl.includes('ast-transforms') && !newUrl.includes('node_modules') && newUrl.includes('/actions/')) { 39 | try { 40 | const newCode = await transformAction(code, filePath); 41 | return { 42 | format: context.format, 43 | source: newCode, 44 | shortCircuit: true, 45 | }; 46 | } 47 | catch (err) { 48 | if (err instanceof ReasonError) { 49 | throw new ActionError(`The following error happened while trying to process the action ${path.basename(filePath)}: ${err.message}`, err.code, { 50 | filePath, 51 | code, 52 | stack: err.stack, 53 | }); 54 | } 55 | throw new ActionError(`An unknown error has happened while trying to process the action ${path.basename(filePath)}, please get in contact.`, 12, { 56 | filePath, 57 | code, 58 | stack: err.stack, 59 | }); 60 | } 61 | } 62 | else if (!newUrl.includes('ast-transforms') && !newUrl.includes('node_modules') && newUrl.includes('/agents/')) { 63 | try { 64 | const newCode = await agentTransform(code, filePath); 65 | return { 66 | format: context.format, 67 | source: newCode, 68 | shortCircuit: true, 69 | }; 70 | } 71 | catch (err) { 72 | if (err instanceof ReasonError) { 73 | throw new AgentError(`The following error happened while trying to process the agent ${path.basename(filePath)}: ${err.message}`, err.code, { 74 | filePath, 75 | code, 76 | stack: err.stack, 77 | }); 78 | } 79 | throw new AgentError(`An unknown error has happened while trying to process the agent ${path.basename(filePath)}, please get in contact.`, 12, { 80 | filePath, 81 | code, 82 | stack: err.stack, 83 | }); 84 | } 85 | } 86 | else { 87 | if (isPathFromReason(code) && code.includes('reason')) { 88 | // can potentially use `think` 89 | try { 90 | const newCode = await thinkTransform(code, filePath); 91 | return { 92 | format: context.format, 93 | source: newCode, 94 | shortCircuit: true, 95 | }; 96 | } 97 | catch (err) { 98 | console.log(err) 99 | if (err instanceof ReasonError) { 100 | throw new ThinkError(`The following error happened while trying to process the \`reason\` call in ${path.basename(filePath)}: ${err.message}`, err.code, { 101 | filePath, 102 | code, 103 | stack: err.stack, 104 | }); 105 | } 106 | throw new ThinkError(`An unknown error has happened while trying to process the \`reason\` call in ${path.basename(filePath)}, please get in contact.`, 12, { 107 | filePath, 108 | code, 109 | err, 110 | stack: err.stack, 111 | }); 112 | } 113 | } 114 | } 115 | return esmLoader.load(url, context, defaultLoad); 116 | } 117 | export async function resolve(specifier, context, nextResolve) { 118 | // if (specifier.toLowerCase().startsWith('c:\\')) { 119 | // specifier = 'file:///' + specifier.replaceAll('\\', '/') 120 | // } 121 | 122 | if (isWindows() && specifier.lastIndexOf('file:/') > 1) { 123 | // const idx = specifier.lastIndexOf('file:/') 124 | // specifier = specifier.slice(0, idx) + specifier.slice(idx + 'file:/'.length) 125 | specifier = 'file:///' + specifier.split('file:/')[2] 126 | } 127 | 128 | if (context.parentURL?.indexOf('node_modules/tryreason') !== -1 && path.extname(specifier) === '' && specifier[0] === '.') { 129 | specifier = specifier + '.js'; 130 | } 131 | return esmLoader.resolve(specifier, context, nextResolve); 132 | } 133 | -------------------------------------------------------------------------------- /configs/anyscale.ts: -------------------------------------------------------------------------------- 1 | import reasonConfig from "./reason-config"; 2 | 3 | const anyscaleConfig = reasonConfig.anyscale 4 | 5 | export { anyscaleConfig } -------------------------------------------------------------------------------- /configs/openai.ts: -------------------------------------------------------------------------------- 1 | import reasonConfig from "./reason-config"; 2 | 3 | const openaiConfig = reasonConfig.openai 4 | 5 | export { openaiConfig } -------------------------------------------------------------------------------- /configs/reason-config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import ReasonError from '../utils/reasonError.js' 3 | import path from 'path' 4 | import ReasonConfig from '../types/reasonConfig.js'; 5 | 6 | interface OpenAIConfig { 7 | defaultModel: string; 8 | key: string 9 | } 10 | 11 | export { OpenAIConfig, ReasonConfig } 12 | 13 | let reasonConfig 14 | 15 | function isWindows() { 16 | return process.platform === 'win32' 17 | } 18 | 19 | try { 20 | if (!fs.existsSync('./.reason.config.js')) { 21 | throw new ReasonError(`Could not find a \`.reason.config.js\` file in the root of your project. Please create one. Learn more at link.docs`, 9018) 22 | } 23 | 24 | let configPath = path.join(process.cwd(), '.reason.config.js') 25 | 26 | // windows really sucks 27 | if (isWindows()) configPath = `file:///${configPath}` 28 | const config = await import(configPath) 29 | reasonConfig = config.default 30 | 31 | // const configFile = fs.readFileSync('./.reason-config.json', 'utf8') 32 | // reasonConfig = JSON.parse(configFile) 33 | } catch (err) { 34 | throw new ReasonError(`Could not read your \`.reason.config.js\` file to get your default configuration. Please make sure it exists and is valid JSON. Learn more at link.docs`, 9019, { err }) 35 | } 36 | 37 | export default reasonConfig as ReasonConfig -------------------------------------------------------------------------------- /functions/__internal/StremableObject.ts: -------------------------------------------------------------------------------- 1 | export default class StreamableObject { 2 | public internal: Record = {} 3 | public value: any; 4 | public done: boolean; 5 | 6 | // this enables the return of `reasonStream` to be extended 7 | // i.e.: `for await (const output of reasonStream(...)) output.newProp = 'foo'` 8 | [key: string]: any; 9 | 10 | constructor(value: any, done: boolean) { 11 | this.value = value 12 | this.done = done 13 | } 14 | } -------------------------------------------------------------------------------- /functions/__internal/__internal_think.ts: -------------------------------------------------------------------------------- 1 | import IContext from "../../observability/context.js"; 2 | import { Trace } from "../../observability/tracer"; 3 | import getChatCompletion, { getChatCompletionGen } from "../../services/getChatCompletion"; 4 | import type { OAIMessage, ThinkConfig } from "../../types/thinkConfig.d.ts"; 5 | import asyncLocalStorage from "../../utils/asyncLocalStorage"; 6 | import stream from "../stream.js"; 7 | import { ReasonStreamReturn } from "../think"; 8 | import extractorThink, { extractorThinkStream } from "./think-extractor"; 9 | 10 | interface ExtractorInfo { 11 | name: string; 12 | type: string; 13 | required: boolean; 14 | prompt?: string; 15 | } 16 | 17 | export { ExtractorInfo } 18 | 19 | export default async function __internal_DO_NOT_USE_think(input: string | OAIMessage[], config: ThinkConfig | null, extractor?: ExtractorInfo[]): Promise { 20 | const context = asyncLocalStorage.getStore() as IContext 21 | 22 | if (extractor) { 23 | const trace = new Trace(context, 'LLM call', 'extractor-llm-call') 24 | return trace.startActiveSpan(async (span: any) => { 25 | const res = await extractorThink(input, config, extractor, trace) 26 | trace.end() 27 | return res 28 | }) 29 | } 30 | 31 | let messages: OAIMessage[] = [] 32 | 33 | if (typeof input === 'string') { 34 | messages.push({ 35 | role: 'user', 36 | content: input, 37 | }) 38 | } else messages = input 39 | 40 | config = config ?? {} 41 | 42 | const trace = new Trace(context, 'LLM call', 'normal-llm-call') 43 | 44 | return trace.startActiveSpan(async (span: any) => { 45 | let res = await getChatCompletion(messages as any, config as any, trace) 46 | trace.end() 47 | return res.content as T 48 | }) 49 | } 50 | 51 | export async function* __internal_DO_NOT_USE_thinkStream(input: string | OAIMessage[], config: ThinkConfig | null, extractor?: ExtractorInfo[]): AsyncGenerator | string, T> { 52 | const context = asyncLocalStorage.getStore() as IContext 53 | 54 | if (extractor) { 55 | const trace = new Trace(context, 'LLM call', 'extractor-llm-call') 56 | trace.startActiveSpan((span: any) => {}) 57 | trace.addAttribute('llm.call.is_stream', true) 58 | 59 | const gen = extractorThinkStream(input, config, extractor, trace) 60 | let result = await gen.next() 61 | while (!result.done) { 62 | let { value } = result 63 | 64 | yield value as ReasonStreamReturn 65 | 66 | result = await gen.next() 67 | } 68 | 69 | const returnValue = result.value 70 | trace.end() 71 | return returnValue as T 72 | } 73 | 74 | let messages: OAIMessage[] = [] 75 | 76 | if (typeof input === 'string') { 77 | messages.push({ 78 | role: 'user', 79 | content: input, 80 | }) 81 | } 82 | 83 | if (Array.isArray(input)) { 84 | messages = input 85 | } 86 | 87 | let model 88 | if (config?.model) { 89 | model = config.model 90 | delete config.model 91 | delete config.validation_strategy 92 | } 93 | 94 | let autoStream = false 95 | if (config?.autoStream) { 96 | autoStream = config.autoStream 97 | delete config.autoStream 98 | } 99 | 100 | let llmconfig = { 101 | model, 102 | config: { 103 | ...config, 104 | } 105 | } 106 | 107 | const trace = new Trace(context, 'LLM call', 'normal-llm-call') 108 | trace.startActiveSpan((span: any) => {}) 109 | trace.addAttribute('llm.call.is_stream', true) 110 | const gen = getChatCompletionGen(messages as any, llmconfig as any, trace) 111 | let result = await gen.next() 112 | let fullText = ''; 113 | 114 | let val: any = { value: '', done: false, delta: '' } 115 | let completionDone = false 116 | 117 | const getcompletion = async () => { 118 | while (!result.done) { 119 | let { value } = result; 120 | 121 | if (typeof(value) !== 'string') { 122 | result = await gen.next(); 123 | continue 124 | } 125 | 126 | fullText += value 127 | val = { value: fullText, done: false, delta: value }; 128 | result = await gen.next(); 129 | 130 | if (autoStream) { 131 | stream(val) 132 | } 133 | } 134 | completionDone = true 135 | 136 | val = { value: result.value, done: true, delta: '' }; 137 | if (autoStream) { 138 | stream(val) 139 | } 140 | 141 | trace.addAttribute('llm.call.output', val.value) 142 | trace.end() 143 | } 144 | 145 | const promises = [getcompletion()] 146 | 147 | while (!completionDone) { 148 | yield val 149 | 150 | /* the reason this is needed is to make sure the user `for await (... of reasonStream()) {}` 151 | does not starve the event loop — blocking all I/O. 152 | 153 | by waiting for a promise that resolves in the next `timers` phase of the event loop 154 | we make sure all I/O is processed before this loop runs again. */ 155 | await new Promise(resolve => setTimeout(resolve, 0)) 156 | } 157 | yield val 158 | 159 | 160 | if (autoStream) { 161 | await Promise.all(promises) 162 | } 163 | 164 | return val as T 165 | } -------------------------------------------------------------------------------- /functions/agent.ts: -------------------------------------------------------------------------------- 1 | import type ReasonAgent from "../types/iagent.d.ts"; 2 | 3 | export default async function useAgent(memoryID?: string): Promise { return {} as ReasonAgent } -------------------------------------------------------------------------------- /functions/index.ts: -------------------------------------------------------------------------------- 1 | import useAgent from "./agent" 2 | import reason, { ReasonStreamReturn, reasonStream } from "./think" 3 | import stream from "./stream" 4 | 5 | import __internal_DO_NOT_USE_think, { __internal_DO_NOT_USE_thinkStream } from "./__internal/__internal_think" 6 | import __internal_DO_NOT_USE_useAgent from "./__internal/__internal_agent" 7 | import ReasonConfig from "../types/reasonConfig" 8 | import AgentConfig from "../types/agentConfig" 9 | import ActionConfig from "../types/actionConfig" 10 | 11 | export { 12 | useAgent, 13 | reason, 14 | reasonStream, 15 | stream, 16 | __internal_DO_NOT_USE_think as __internal_DO_NOT_USE_reason, 17 | __internal_DO_NOT_USE_thinkStream as __internal_DO_NOT_USE_reasonStream, 18 | __internal_DO_NOT_USE_useAgent, 19 | ReasonConfig, 20 | AgentConfig, 21 | ActionConfig, 22 | ReasonStreamReturn, 23 | } -------------------------------------------------------------------------------- /functions/stream.ts: -------------------------------------------------------------------------------- 1 | import { reasonStream } from "."; 2 | import IContext from "../observability/context"; 3 | import asyncLocalStorage from "../utils/asyncLocalStorage"; 4 | import ReasonError from "../utils/reasonError.js"; 5 | 6 | export default function stream(value: string | Record) { 7 | const context = asyncLocalStorage.getStore() as IContext 8 | 9 | // assertion check 10 | if (typeof(value) !== 'string' && typeof(value) !== 'object') { 11 | throw new ReasonError(`The \`stream()\` function only accepts \`string\` or \`object\` as arguments, but got \`${typeof(value)}\`.\n\nLearn more at DOCS_URL\n\n`, 189) 12 | } 13 | if (typeof(value) === 'object') { 14 | if (Array.isArray(value)) { 15 | throw new ReasonError(`The \`stream()\` function only accepts \`string\` or \`object\` as arguments, but got \`array\`.\n\nLearn more at DOCS_URL\n\n`, 183) 16 | } 17 | 18 | if (value === null) { 19 | throw new ReasonError(`The \`stream()\` function only accepts \`string\` or \`object\` as arguments, but got \`null\`.\n\nLearn more at DOCS_URL\n\n`, 184) 20 | } 21 | 22 | if (value instanceof Date) { 23 | throw new ReasonError(`The \`stream()\` function only accepts \`string\` or \`object\` as arguments, but got \`Date\`.\n\nLearn more at DOCS_URL\n\n`, 185) 24 | } 25 | } 26 | 27 | if (!context.stream) { 28 | // TODO: add link to docs 29 | throw new ReasonError(`You tried using the \`stream()\` function in a non-streamable entrypoint (\`${context.entrypointName}\`).\nTo make it streamable, just turn the function into an async generator function: \`async function* handler() {}\`.\n\nLearn more at DOCS_URL\n\n`, 160) 30 | } 31 | 32 | context.stream.send(value) 33 | } -------------------------------------------------------------------------------- /functions/think.ts: -------------------------------------------------------------------------------- 1 | import Streamable, { StreamableDone, StreamableNotDone } from "../types/streamable"; 2 | import { OAIMessage, ThinkConfig } from "../types/thinkConfig"; 3 | 4 | // this enables the return of `reasonStream` to be extended 5 | // i.e.: `for await (const output of reasonStream(...)) output.newProp = 'foo'` 6 | // this alsos enables the return of `reasonStream` to have nested StreamableValues in them 7 | type ActualStreamReturnDone = T extends string | number | boolean ? StreamableDone : (StreamableDone<{ 8 | [K in keyof T]: T[K] extends Array ? Array> : ActualStreamReturnDone; 9 | }> 10 | ) & Streamable<{ 11 | [key: string]: unknown; 12 | }>; 13 | 14 | type ActualStreamReturnNotDone = T extends string | number | boolean ? Streamable : (StreamableNotDone<{ 15 | [K in keyof T]: T[K] extends Array ? Array> : ActualStreamReturnNotDone; 16 | }> 17 | ) & Streamable<{ 18 | [key: string]: unknown; 19 | }>; 20 | 21 | type ActualStreamReturn = T extends string | number | boolean ? Streamable : (StreamableDone<{ 22 | [K in keyof T]: T[K] extends Array ? Array> : ActualStreamReturnDone; 23 | }> 24 | | StreamableNotDone<{ 25 | [K in keyof T]: T[K] extends Array ? Array> : ActualStreamReturnNotDone; 26 | }> 27 | ) & Streamable<{ 28 | [key: string]: unknown; 29 | }>; 30 | 31 | type ReasonStreamReturn = T extends string | number | boolean ? Streamable : { 32 | [K in keyof T]: ActualStreamReturn 33 | } & { 34 | [key: string]: unknown; 35 | } 36 | 37 | export { ReasonStreamReturn } 38 | 39 | type StreamFunction = (values: ReasonStreamReturn) => void 40 | 41 | export default function reason(prompt: string, config?: ThinkConfig): Promise; 42 | export default function reason(messages: OAIMessage[], config?: ThinkConfig): Promise; 43 | 44 | export default async function reason(input: string | OAIMessage[], config?: ThinkConfig): Promise { return {} as T } 45 | 46 | interface ReasonStreamStringOutput { 47 | done: false; 48 | value: string; 49 | delta: string; 50 | } 51 | 52 | export { ReasonStreamStringOutput } 53 | 54 | // @ts-expect-error 55 | export function reasonStream(prompt: string, config?: ThinkConfig): AsyncGenerator; 56 | export function reasonStream(prompt: string, config?: ThinkConfig): AsyncGenerator, T>; 57 | export function reasonStream(messages: OAIMessage[], config?: ThinkConfig): AsyncGenerator, T>; 58 | 59 | export async function* reasonStream(input: string | OAIMessage[], config?: ThinkConfig): AsyncGenerator, T> { return {} as T } -------------------------------------------------------------------------------- /observability/context.d.ts: -------------------------------------------------------------------------------- 1 | interface ILLMCallInfo { 2 | prompt: 'OpenAI'; 3 | model: string; 4 | prompt: string; 5 | completion: string; 6 | temperature: number; 7 | tokens_usage: { 8 | prompt: number; 9 | completion: number; 10 | total: number; 11 | } 12 | timestamps: { 13 | start: Date; 14 | end?: Date; 15 | } 16 | } 17 | 18 | interface IAction { 19 | id: string; 20 | name: string; 21 | input: unknown; 22 | output: unknown; 23 | timestamps: { 24 | start: Date; 25 | end?: Date; 26 | } 27 | actions: IAction[]; 28 | 29 | llm_calls?: ILLMCallInfo[]; 30 | tokens_usage?: { 31 | prompt: number; 32 | completion: number; 33 | total: number; 34 | } 35 | 36 | // internal fields that REASON needs — will be striped before sending to the observability service 37 | __internal_done: boolean; 38 | } 39 | 40 | interface IContext extends IAction { 41 | id: string; 42 | entrypointName: string; 43 | span: any; 44 | spans: Record; 45 | url: string; 46 | headers: Record; 47 | body: unknown; 48 | currentAction: IAction; 49 | stop?: boolean; 50 | hasErrored?: boolean; 51 | stream?: { 52 | send: (data: string | Record) => void; 53 | }; 54 | } 55 | 56 | export default IContext 57 | 58 | export type { IAction, ILLMCallInfo } 59 | 60 | 61 | // there is no current *global* action 62 | // what there is is current executing actions 63 | // and for a given context there is only one current action being executed at a time -------------------------------------------------------------------------------- /observability/createContext.ts: -------------------------------------------------------------------------------- 1 | import IContext from "./context"; 2 | 3 | export default function createContext(req: Request, entrypointName: string): IContext { 4 | const context: IContext = { 5 | id: Math.random().toString().replace('0.', ''), 6 | // url: req.url, 7 | entrypointName: entrypointName.replaceAll('-', ' '), 8 | timestamps: { 9 | start: new Date(), 10 | }, 11 | spans: {}, 12 | actions: [], 13 | get currentAction() { 14 | return this.actions[0] ?? null 15 | } 16 | } as any 17 | 18 | return context 19 | } 20 | 21 | /* 22 | 23 | [ 24 | entry -> classify -> getwebpage (processing) 25 | entry -> 26 | ] 27 | 28 | */ -------------------------------------------------------------------------------- /observability/setup-opentel.ts: -------------------------------------------------------------------------------- 1 | import { NodeSDK } from '@opentelemetry/sdk-node'; 2 | import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; 3 | import { Resource } from '@opentelemetry/resources'; 4 | import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; 5 | import reasonConfig from '../configs/reason-config'; 6 | 7 | export default function setupOpenTEL() { 8 | let exporter: any; 9 | if (reasonConfig.openTelemetryExporter) { 10 | console.log(`RΞASON — Using custom OpenTelemetry exporter`) 11 | exporter = reasonConfig.openTelemetryExporter; 12 | } else { 13 | exporter = new ZipkinExporter({ 14 | url: 'http://localhost:9411/api/v2/spans', 15 | }); 16 | } 17 | 18 | const sdk = new NodeSDK({ 19 | resource: new Resource({ 20 | [SemanticResourceAttributes.SERVICE_NAME]: 'app', 21 | [SemanticResourceAttributes.SERVICE_VERSION]: '1.0', 22 | }), 23 | traceExporter: exporter, 24 | }); 25 | 26 | sdk.start(); 27 | } -------------------------------------------------------------------------------- /observability/tracer.ts: -------------------------------------------------------------------------------- 1 | import { SpanStatusCode, trace } from '@opentelemetry/api'; 2 | import type IContext from './context.d.ts'; 3 | import { Span } from '@opentelemetry/api'; 4 | 5 | const tracer = trace.getTracer('app', '1.0.0'); 6 | 7 | export default tracer 8 | 9 | export function addAttribute(span: any, key: string, value: any) { 10 | function setSpanAttribute(obj: any, currentKey: string) { 11 | if (typeof obj === 'object' && obj !== null) { 12 | for (const [subKey, subValue] of Object.entries(obj)) { 13 | setSpanAttribute(subValue, `${currentKey}.${subKey}`); 14 | } 15 | } else { 16 | span.setAttribute(currentKey, obj); 17 | } 18 | } 19 | 20 | setSpanAttribute(value, key); 21 | } 22 | 23 | type TraceTypes = 24 | 'normal-llm-call' 25 | | 'entrypoint' 26 | | 'extractor-llm-call' 27 | | 'agent-llm-call' 28 | | 'agent' 29 | | 'agent-step' 30 | | 'action' 31 | 32 | export class Trace { 33 | public span!: Span 34 | public tracer: any 35 | private context: IContext; 36 | private id?: string; 37 | private name: string; 38 | private type: TraceTypes; 39 | private isCustomServer: boolean = false; 40 | 41 | constructor(context: IContext, name: string, type: TraceTypes) { 42 | if (!name) name = 'app' 43 | this.tracer = trace.getTracer(name, '1.0.0'); 44 | 45 | this.name = name 46 | this.type = type 47 | this.context = context 48 | 49 | if (!context) this.isCustomServer = true 50 | } 51 | 52 | private registerSpan() { 53 | if (this.isCustomServer) return 54 | 55 | const id = this.span.spanContext().spanId 56 | this.id = id 57 | this.context.spans[id] = this.span 58 | } 59 | 60 | err(err: any) { 61 | if (this.isCustomServer) return 62 | 63 | this.span.setStatus({ code: SpanStatusCode.ERROR, message: err?.message }) 64 | this.end() 65 | } 66 | 67 | end() { 68 | if (this.isCustomServer) return 69 | 70 | if (this.id) { 71 | delete this.context.spans[this.id] 72 | } 73 | this.span.end() 74 | } 75 | 76 | async startActiveSpan(fn: Function) { 77 | if (this.isCustomServer) { 78 | let result = await fn({ name: 'empty-span' }) 79 | return result 80 | } 81 | 82 | return this.tracer.startActiveSpan(this.name, async (span: any) => { 83 | this.span = span 84 | this.registerSpan() 85 | this.addAttribute('reason.type', this.type) 86 | 87 | let result = await fn(span) 88 | return result 89 | }) 90 | } 91 | 92 | startActiveSpanSync(fn: Function) { 93 | if (this.isCustomServer) { 94 | let result = fn({ name: 'empty-span' }) 95 | return result 96 | } 97 | 98 | return this.tracer.startActiveSpan(this.name, (span: any) => { 99 | this.span = span 100 | this.registerSpan() 101 | this.addAttribute('reason.type', this.type) 102 | 103 | let result = fn(span) 104 | return result 105 | }) 106 | } 107 | 108 | addAttribute(key: string, value: any) { 109 | if (this.isCustomServer) return 110 | 111 | this.setSpanAttribute(value, `_${key}`); 112 | } 113 | 114 | setSpanAttribute(obj: any, currentKey: string, attributes: Record = {}) { 115 | if (this.isCustomServer) return 116 | 117 | if (typeof obj === 'object' && obj !== null) { 118 | for (let [subKey, subValue] of Object.entries(obj)) { 119 | if (Array.isArray(obj)) { 120 | subKey = `[${subKey}]` 121 | } 122 | this.setSpanAttribute(subValue, `${currentKey}.${subKey}`); 123 | } 124 | } else { 125 | this.span.setAttribute(currentKey, obj); 126 | } 127 | } 128 | 129 | getOTELattributes(obj: any, currentKey: string, attributes: Record = {}) { 130 | if (this.isCustomServer) return 131 | 132 | if (typeof obj === 'object' && obj !== null) { 133 | for (let [subKey, subValue] of Object.entries(obj)) { 134 | if (Array.isArray(obj)) { 135 | subKey = `[${subKey}]` 136 | } 137 | this.getOTELattributes(subValue, `${currentKey}.${subKey}`); 138 | } 139 | } else { 140 | attributes[currentKey] = obj; 141 | } 142 | 143 | return attributes 144 | } 145 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tryreason", 3 | "version": "0.0.21-alpha", 4 | "main": "dist/functions/index.js", 5 | "license": "MIT", 6 | "type": "module", 7 | "bin": { 8 | "reason": "./commands/reason-command.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc && cp -f -r types/ dist/types/ && mv -f dist/compiler/ts-dev-mode-loader.mjs dist/compiler/ts-dev-mode-loader.js", 12 | "prepack": "rm -rf dist && npm run build && cd web && npm i && npm run build", 13 | "postinstall": "node ./scripts/post-install.js" 14 | }, 15 | "devDependencies": { 16 | "@types/cors": "2.8.15", 17 | "@types/express": "4.17.18", 18 | "@types/set-cookie-parser": "^2.4.5", 19 | "bun-types": "1.0.3" 20 | }, 21 | "peerDependencies": { 22 | "nodemon": "^3.0.1", 23 | "typescript": "5.2.2" 24 | }, 25 | "dependencies": { 26 | "@babel/core": "7.23.0", 27 | "@babel/parser": "7.23.0", 28 | "@babel/preset-typescript": "7.23.0", 29 | "@babel/traverse": "7.23.0", 30 | "@opentelemetry/api": "1.6.0", 31 | "@opentelemetry/core": "1.17.1", 32 | "@opentelemetry/exporter-zipkin": "1.17.1", 33 | "@opentelemetry/resources": "1.17.1", 34 | "@opentelemetry/sdk-node": "0.44.0", 35 | "@opentelemetry/semantic-conventions": "1.17.1", 36 | "@swc-node/register": "1.6.7", 37 | "@swc/core": "1.3.91", 38 | "@types/jsdom": "21.1.4", 39 | "ajv": "8.12.0", 40 | "ansi-colors": "^4.1.3", 41 | "bindings": "1.5.0", 42 | "cors": "2.8.5", 43 | "enhanced-resolve": "5.15.0", 44 | "enquirer": "^2.4.1", 45 | "express": "4.18.2", 46 | "fs-extra": "^11.1.1", 47 | "kysely": "0.26.3", 48 | "open": "^9.1.0", 49 | "readline-sync": "^1.4.10", 50 | "set-cookie-parser": "2.6.0", 51 | "sqlite3": "5.1.6", 52 | "typescript": "5.2.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/post-install.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url'; 2 | import { dirname, join } from 'path'; 3 | import { spawn } from 'child_process' 4 | 5 | const currentDirectory = dirname(fileURLToPath(import.meta.url)); 6 | const playgroundPath = join(currentDirectory, '..', 'web'); 7 | console.log('rodado!!!!!', playgroundPath); 8 | const command = spawn(`cd ${playgroundPath} && npm install --production`, { shell: true, stdio: 'inherit' }) 9 | console.log('rodado!!!!!', playgroundPath); -------------------------------------------------------------------------------- /server/entrypoints.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import ServerError from './serverError'; 4 | 5 | let entrypointsPath: string 6 | if (fs.existsSync(path.join(process.cwd(), 'src', 'entrypoints'))) entrypointsPath = path.join(process.cwd(), 'src', 'entrypoints') 7 | else if (fs.existsSync(path.join(process.cwd(), 'entrypoints'))) entrypointsPath = path.join(process.cwd(), 'entrypoints') 8 | else { 9 | throw new ServerError(`Could not find your entrypoints folder.\nTried: ${path.join(process.cwd(), 'src', 'entrypoints')} and ${path.join(process.cwd(), 'entrypoints')}`, 9916) 10 | } 11 | 12 | const paths: Record = {} 13 | 14 | interface IDirNode { 15 | name: string 16 | ext: null; 17 | isFile: false 18 | isDirectory: true 19 | serverpath: string 20 | systempath: string 21 | children: IDirNode[] 22 | } 23 | 24 | interface IFileNode { 25 | name: string 26 | ext: string; 27 | isFile: true 28 | isDirectory: false 29 | serverpath: string 30 | systempath: string 31 | } 32 | 33 | type INode = IDirNode | IFileNode 34 | 35 | const VALID_EXTENSIONS = ['ts', 'js'] 36 | 37 | function getNodes(systempath: string, serverpath: string): INode[] { 38 | let files = fs.readdirSync(systempath, { withFileTypes: true }) 39 | 40 | let nodes: INode[] = [] 41 | for (const file of files) { 42 | if (!file.isFile() && !file.isDirectory()) continue 43 | 44 | let name = file.name 45 | let ext = null 46 | if (file.isFile()) { 47 | const extension = path.extname(name) 48 | name = name.replace(extension, '') 49 | ext = extension.replace('.', '') 50 | 51 | if (VALID_EXTENSIONS.includes(ext) === false) { 52 | continue 53 | } 54 | } 55 | 56 | const node = { 57 | name, 58 | ext, 59 | isFile: file.isFile(), 60 | isDirectory: file.isDirectory(), 61 | serverpath: `${serverpath}/${name}`, 62 | systempath: path.join(systempath, file.name), 63 | } as INode 64 | 65 | nodes.push(node) 66 | } 67 | 68 | return nodes 69 | } 70 | 71 | interface IEntrypoint { 72 | prettyName: string; 73 | method: string 74 | serverPath: string; 75 | handler: Function; 76 | } 77 | 78 | export { IEntrypoint } 79 | 80 | async function getEntrypoints(): Promise { 81 | const entrypoints: IEntrypoint[] = [] 82 | 83 | let nodes = getNodes(entrypointsPath, '') 84 | 85 | let node 86 | // dfs 87 | while (nodes.length >= 1) { 88 | node = nodes.pop() 89 | if (!node) break 90 | 91 | assertName(node.name) 92 | 93 | // handler [param] files/folders 94 | if (node.name[0] === '[') { 95 | const closingBracketIndex = node.name.indexOf(']') 96 | const insideBrackets = node.name.slice(1, closingBracketIndex) 97 | node.serverpath = node.serverpath.replace(`[${insideBrackets}]`, `:${insideBrackets}`) 98 | } 99 | 100 | if (node.isDirectory) { 101 | let subnodes = getNodes(node.systempath, node.serverpath) 102 | nodes.push(...subnodes) 103 | continue 104 | } 105 | 106 | // handle index files 107 | if (node.name === 'index') { 108 | node.serverpath = node.serverpath.replace('/index', '') 109 | 110 | if (node.serverpath === '') { 111 | node.serverpath = '/' 112 | } 113 | } 114 | 115 | 116 | // check for duplicate entrypoint path 117 | if (paths[node.serverpath]) { 118 | throw new ServerError(`Duplicate entrypoint name ${node.serverpath} — you cannot have two entrypoints with the same path.`, 1, { 119 | node, 120 | paths, 121 | entrypoints 122 | }) 123 | } 124 | 125 | paths[node.serverpath] = true 126 | 127 | const entrypointCode = await import(node.systempath) 128 | const handler = entrypointCode.default 129 | const methods = [] 130 | if (typeof handler === 'function') methods.push({name: 'POST', fn: handler}) 131 | if (typeof entrypointCode.GET === 'function') methods.push({name: 'GET', fn: entrypointCode.GET}) 132 | if (typeof entrypointCode.POST === 'function' && typeof handler !== 'function') methods.push({name: 'POST', fn: entrypointCode.POST}) 133 | if (typeof entrypointCode.PUT === 'function') methods.push({name: 'PUT', fn: entrypointCode.PUT}) 134 | if (typeof entrypointCode.DELETE === 'function') methods.push({name: 'DELETE', fn: entrypointCode.DELETE}) 135 | 136 | if (methods.length === 0) { 137 | throw new ServerError(`Entrypoint ${node.serverpath} does not export an entrypoint function.`, 2, { 138 | node, 139 | entrypoints 140 | }) 141 | } 142 | const prettyName = node.name.replace('-', ' ') 143 | for (const method of methods) { 144 | let entrypoint: IEntrypoint = { 145 | handler: method.fn, 146 | method: method.name, 147 | prettyName, 148 | serverPath: node.serverpath, 149 | } 150 | 151 | entrypoints.push(entrypoint) 152 | } 153 | } 154 | 155 | return entrypoints 156 | } 157 | 158 | // a name is only valid if it has letters and dashes 159 | // everything else is invalid 160 | function assertName(name: string): boolean { 161 | if (name[0] === '[') { 162 | const closingBracketIndex = name.indexOf(']') 163 | 164 | if (closingBracketIndex === -1 || closingBracketIndex !== name.length - 1) { 165 | throw new ServerError(`Invalid entrypoint name for ${name}.\nEntrypoint names that start with '[' must end with ']'`, 3, { name }) 166 | } 167 | 168 | const insideBrackets = name.slice(1, closingBracketIndex) 169 | return /^[a-z-]+$/.test(insideBrackets) 170 | } 171 | 172 | if (name[0] === '-') { 173 | throw new ServerError(`Invalid entrypoint name for ${name}.\nEntrypoint names cannot start with '-'`, 4, { name }) 174 | } 175 | 176 | if (/^[a-z-]+$/.test(name) === false) { 177 | throw new ServerError(`Invalid entrypoint name for ${name}.\nEntrypoint names can only contain lowercase letters and dashes — or be wrapped in square brackets.`, 5, { name }) 178 | } 179 | 180 | return true 181 | } 182 | 183 | export { getEntrypoints } -------------------------------------------------------------------------------- /server/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { SpanStatusCode } from '@opentelemetry/api'; 2 | import IContext from "../observability/context"; 3 | import asyncLocalStorage from "../utils/asyncLocalStorage"; 4 | import isDebug from "../utils/isDebug.js"; 5 | import ReasonError from "../utils/reasonError.js"; 6 | 7 | function stopAllSpans(context: IContext, err: any) { 8 | for (const span of Object.values(context.spans)) { 9 | span.setStatus({ code: SpanStatusCode.ERROR, message: err?.message }) 10 | span.end() 11 | } 12 | } 13 | 14 | export default function errorhandler(err: any, ctx?: IContext) { 15 | let context: IContext 16 | 17 | if (!ctx) context = asyncLocalStorage.getStore() as IContext 18 | else context = ctx 19 | 20 | context.span?.setStatus({ code: SpanStatusCode.ERROR, message: err?.message }) 21 | context.span?.end() 22 | 23 | stopAllSpans(context, err) 24 | 25 | context.stop = true 26 | context.hasErrored = true 27 | 28 | console.error(`[ERROR ${context.entrypointName}]: ${err?.message}`); 29 | 30 | if (isDebug) { 31 | console.log('-------STACK TRACE-------'); 32 | console.log(err?.stack); 33 | } 34 | 35 | if (err instanceof ReasonError) { 36 | if (isDebug) { 37 | console.log('-------ADDITIONAL INFORMATION-------'); 38 | console.log(JSON.stringify(err.debug_info, null, 2)); 39 | } 40 | 41 | return 42 | } 43 | 44 | console.error(err?.stack); 45 | } -------------------------------------------------------------------------------- /server/fetch-standard.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'http'; 2 | import { splitCookiesString } from 'set-cookie-parser'; 3 | 4 | const clientAddressSymbol = Symbol.for('astro.clientAddress'); 5 | 6 | /* 7 | Credits to the SvelteKit team 8 | https://github.com/sveltejs/kit/blob/8d1ba04825a540324bc003e85f36559a594aadc2/packages/kit/src/exports/node/index.js 9 | */ 10 | 11 | /* 12 | and credits to the Astro team (which is where i found this lol) 13 | https://github.com/withastro/astro/blob/2dca81bf2174cd5c27cb63cb0ae081ea2a1ac771/packages/integrations/vercel/src/serverless/request-transform.ts 14 | */ 15 | 16 | function get_raw_body(req: IncomingMessage, body_size_limit?: number): ReadableStream | null { 17 | const h = req.headers; 18 | 19 | if (!h['content-type']) { 20 | return null; 21 | } 22 | 23 | const content_length = Number(h['content-length']); 24 | 25 | // check if no request body 26 | if ( 27 | (req.httpVersionMajor === 1 && isNaN(content_length) && h['transfer-encoding'] == null) || 28 | content_length === 0 29 | ) { 30 | return null; 31 | } 32 | 33 | let length = content_length; 34 | 35 | if (body_size_limit) { 36 | if (!length) { 37 | length = body_size_limit; 38 | } else if (length > body_size_limit) { 39 | throw new HTTPError( 40 | 413, 41 | `Received content-length of ${length}, but only accept up to ${body_size_limit} bytes.` 42 | ); 43 | } 44 | } 45 | 46 | if (req.destroyed) { 47 | const readable = new ReadableStream(); 48 | readable.cancel(); 49 | return readable; 50 | } 51 | 52 | let size = 0; 53 | let cancelled = false; 54 | 55 | return new ReadableStream({ 56 | start(controller) { 57 | req.on('error', (error) => { 58 | cancelled = true; 59 | controller.error(error); 60 | }); 61 | 62 | req.on('end', () => { 63 | if (cancelled) return; 64 | controller.close(); 65 | }); 66 | 67 | req.on('data', (chunk) => { 68 | if (cancelled) return; 69 | 70 | size += chunk.length; 71 | if (size > length) { 72 | cancelled = true; 73 | controller.error( 74 | new HTTPError( 75 | 413, 76 | `request body size exceeded ${ 77 | content_length ? "'content-length'" : 'BODY_SIZE_LIMIT' 78 | } of ${length}` 79 | ) 80 | ); 81 | return; 82 | } 83 | 84 | controller.enqueue(chunk); 85 | 86 | if (controller.desiredSize === null || controller.desiredSize <= 0) { 87 | req.pause(); 88 | } 89 | }); 90 | }, 91 | 92 | pull() { 93 | req.resume(); 94 | }, 95 | 96 | cancel(reason) { 97 | cancelled = true; 98 | req.destroy(reason); 99 | }, 100 | }); 101 | } 102 | 103 | export async function getRequest( 104 | base: string, 105 | req: IncomingMessage, 106 | ip?: string, 107 | bodySizeLimit?: number 108 | ): Promise { 109 | let headers = req.headers as Record; 110 | let request = new Request(base + req.url, { 111 | // @ts-expect-error 112 | duplex: 'half', 113 | method: req.method, 114 | headers: { 115 | ...headers, 116 | 'Reason-Connecting-IP': ip ?? 'unknown', 117 | }, 118 | body: get_raw_body(req, bodySizeLimit), 119 | }); 120 | Reflect.set(request, clientAddressSymbol, headers['x-forwarded-for']); 121 | return request; 122 | } 123 | 124 | export async function setResponse( 125 | // app: App, 126 | res: ServerResponse, 127 | response: Response 128 | ): Promise { 129 | const headers = Object.fromEntries(response.headers); 130 | let cookies: string[] = []; 131 | 132 | if (response.headers.has('set-cookie')) { 133 | const header = response.headers.get('set-cookie')!; 134 | const split = splitCookiesString(header); 135 | cookies = split; 136 | } 137 | 138 | // if (app.setCookieHeaders) { 139 | // for (const setCookieHeader of app.setCookieHeaders(response)) { 140 | // cookies.push(setCookieHeader); 141 | // } 142 | // } 143 | 144 | res.writeHead(response.status, { ...headers, 'set-cookie': cookies }); 145 | 146 | if (!response.body) { 147 | res.end(); 148 | return; 149 | } 150 | 151 | if (response.body.locked) { 152 | res.write( 153 | 'Fatal error: Response body is locked. ' + 154 | `This can happen when the response was already read (for example through 'response.json()' or 'response.text()').` 155 | ); 156 | res.end(); 157 | return; 158 | } 159 | 160 | const reader = response.body.getReader(); 161 | 162 | if (res.destroyed) { 163 | reader.cancel(); 164 | return; 165 | } 166 | 167 | const cancel = (error?: Error) => { 168 | res.off('close', cancel); 169 | res.off('error', cancel); 170 | 171 | // If the reader has already been interrupted with an error earlier, 172 | // then it will appear here, it is useless, but it needs to be catch. 173 | reader.cancel(error).catch(() => {}); 174 | if (error) res.destroy(error); 175 | }; 176 | 177 | res.on('close', cancel); 178 | res.on('error', cancel); 179 | 180 | next(); 181 | async function next() { 182 | try { 183 | for (;;) { 184 | const { done, value } = await reader.read(); 185 | 186 | if (done) break; 187 | 188 | if (!res.write(value)) { 189 | res.once('drain', next); 190 | return; 191 | } 192 | } 193 | res.end(); 194 | } catch (error) { 195 | cancel(error instanceof Error ? error : new Error(String(error))); 196 | } 197 | } 198 | } 199 | 200 | class HTTPError extends Error { 201 | status: number; 202 | 203 | constructor(status: number, reason: string) { 204 | super(reason); 205 | this.status = status; 206 | } 207 | 208 | get reason() { 209 | return super.message; 210 | } 211 | } -------------------------------------------------------------------------------- /server/handlers/asyncFunctionHandler.ts: -------------------------------------------------------------------------------- 1 | import IContext from "../../observability/context" 2 | import { Trace } from "../../observability/tracer" 3 | import { IEntrypoint } from "../entrypoints" 4 | 5 | function isGenerator(gen: any) { 6 | try { 7 | if (!gen) return false; 8 | return typeof gen.next === 'function' && typeof gen.throw === 'function' && typeof gen.return === 'function'; 9 | } catch { 10 | return false 11 | } 12 | } 13 | 14 | export default async function asyncFunctionHandler(context: IContext, req: Request, entrypoint: IEntrypoint): Promise { 15 | const handler = entrypoint.handler 16 | let body = {} 17 | if (req.headers.get('content-type')?.includes('application/json')) body = await req.clone().json() 18 | 19 | const tracer = new Trace(context, entrypoint.serverPath, 'entrypoint') 20 | 21 | return await tracer.startActiveSpan(async (span: any) => { 22 | context.span = span 23 | tracer.addAttribute('entrypoint.request.body', body) 24 | 25 | try { 26 | const result = await handler(req) 27 | 28 | if (isGenerator(result)) { 29 | throw new Error(`The entrypoint ${entrypoint.serverPath} returned a generator, however this is not supported as this is not a streaming entrypoint. To make it a streaming generator declare like: \`async function* someName() {}\`.\n\n Learn more at https://docs.tryreason.dev/docs/essentials/entrypoints#types-of-entrypoints\n\n(Error code: 994)`) 30 | } 31 | 32 | tracer.addAttribute('entrypoint.response', result) 33 | tracer.end() 34 | if (result instanceof Response) { 35 | return result 36 | } 37 | if (typeof result === 'object') { 38 | // res.json(result) 39 | return new Response(JSON.stringify(result), { 40 | headers: { 41 | 'Content-Type': 'application/json' 42 | } 43 | }) 44 | } 45 | else if (typeof result === 'string') { 46 | // res.send(result + '\n') 47 | return new Response(result + '\n', { headers: { 48 | 'Content-Type': 'text/html' 49 | } }) 50 | } 51 | else { 52 | // res.send(result.toString) 53 | return new Response(result.toString(), { headers: { 54 | 'Content-Type': 'text/html' 55 | } }) 56 | } 57 | } catch (e: any) { 58 | tracer.err(e) 59 | if (e.message) console.error(`[ERROR] ${entrypoint.serverPath}: ${e?.message}`); 60 | else console.error(`[ERROR] ${entrypoint.serverPath}: ${e}`); 61 | // res.status(500).send('Internal Server Error') 62 | return new Response('Internal server error', { status: 500 }) 63 | } 64 | }) 65 | } -------------------------------------------------------------------------------- /server/handlers/asyncGeneratorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream' 2 | import { IEntrypoint } from "../entrypoints"; 3 | import StreamEncoder from "../StreamEncoder"; 4 | import asyncLocalStorage from "../../utils/asyncLocalStorage"; 5 | import IContext from "../../observability/context"; 6 | import ReasonError from "../../utils/reasonError.js"; 7 | import { Trace } from "../../observability/tracer"; 8 | import c from 'ansi-colors' 9 | import errorhandler from '../error-handler'; 10 | 11 | function isGenerator(gen: any) { 12 | try { 13 | if (!gen) return false; 14 | return typeof gen.next === 'function' && typeof gen.throw === 'function' && typeof gen.return === 'function'; 15 | } catch { 16 | return false 17 | } 18 | } 19 | 20 | export default async function asyncGeneratorHandler(req: Request, entrypoint: IEntrypoint): Promise { 21 | const context = asyncLocalStorage.getStore() as IContext 22 | const trace = new Trace(context, `${entrypoint.serverPath}`, 'entrypoint') 23 | 24 | return await trace.startActiveSpan(async (span: any) => { 25 | context.span = span 26 | trace.addAttribute('entrypoint.name', entrypoint.prettyName) 27 | trace.addAttribute('entrypoint.is_stream', true) 28 | trace.addAttribute('entrypoint.request.body', req.body) 29 | 30 | // res.setHeader('Content-Type', 'text/plain; charset=utf-8') 31 | 32 | const encoder = new StreamEncoder() 33 | 34 | console.log(`${c.gray.bold('RΞASON')} — ${c.cyan.italic(entrypoint.method)} ${c.cyan.italic(entrypoint.serverPath)} was called and result will be streamed...`) 35 | let gen = entrypoint.handler(req) 36 | 37 | let rawStream: any = null 38 | 39 | let isReading = false 40 | 41 | const { readable, writable } = new TransformStream(); 42 | 43 | const writer = writable.getWriter(); 44 | 45 | (async () => { 46 | try { 47 | if (isReading) return 48 | isReading = true 49 | let wasStreamClosed = false 50 | writer.closed.then(() => wasStreamClosed = true) 51 | 52 | let result = await gen.next() 53 | let wasReturnGen = false 54 | if (result.done && isGenerator(result.value)) { 55 | wasReturnGen = true 56 | gen = result.value 57 | result = await gen.next() 58 | } 59 | while (!result.done && !wasStreamClosed) { 60 | let { value } = result 61 | if (value !== undefined && value !== null) { 62 | await sendData(value) 63 | } 64 | result = await gen.next(); 65 | } 66 | 67 | if (!wasStreamClosed) { 68 | if (!wasReturnGen) { 69 | if (typeof (result.value) === 'string') { 70 | await sendData(result.value); 71 | } else if (result.value !== undefined && result.value !== null) { 72 | await sendData(result.value); 73 | } 74 | } 75 | writer.close() 76 | isReading = false 77 | trace.addAttribute('entrypoint.response', encoder.internalObj) 78 | trace.end() 79 | return 80 | } 81 | 82 | isReading = false 83 | } catch (err: any) { 84 | isReading = false 85 | if (err instanceof ReasonError && err.code === 1704) return 86 | 87 | if (!context.hasErrored) { 88 | errorhandler(err, context) 89 | await writer.write(encoder.encodeError()) 90 | writer.close() 91 | } 92 | 93 | throw err 94 | } 95 | })(); 96 | 97 | async function sendData(value: string | Record) { 98 | try { 99 | if (context.stop) { 100 | return 101 | } 102 | 103 | if (rawStream === null) { 104 | if (typeof(value) === 'string') rawStream = true 105 | else rawStream = false 106 | } 107 | 108 | if (rawStream) { 109 | if (typeof(value) !== 'string') { 110 | throw new Error(`Expected a string but got ${typeof value}`) 111 | } 112 | 113 | await writer.write(value) 114 | } else { 115 | if (typeof(value) !== 'object') { 116 | throw new Error(`Expected an object but got ${typeof value}`) 117 | } 118 | 119 | const deltas = encoder.encode(value) 120 | const encodedValue = encoder.encodeDeltas(deltas) 121 | 122 | await writer.write(encodedValue) 123 | } 124 | } catch (err: any) { 125 | if (context.stop) return 126 | 127 | if (!context.hasErrored) { 128 | errorhandler(err, context) 129 | await writer.write(encoder.encodeError()) 130 | writer.close() 131 | } 132 | 133 | if (err instanceof ReasonError) { 134 | err.debug_info = { ...err.debug_info, sendData_value: value } 135 | } 136 | 137 | throw err 138 | } 139 | } 140 | 141 | context.stream = { 142 | send: sendData, 143 | } 144 | 145 | return new Response(readable, { 146 | headers: { 'Content-Type': 'text/plain; charset=utf-8' }, 147 | }); 148 | // readable.pipe(res) 149 | }) 150 | } -------------------------------------------------------------------------------- /server/handlers/entrypointHandler.ts: -------------------------------------------------------------------------------- 1 | import createContext from '../../observability/createContext'; 2 | import tracer, { addAttribute } from '../../observability/tracer'; 3 | import asyncLocalStorage from '../../utils/asyncLocalStorage'; 4 | import { IEntrypoint } from '../entrypoints'; 5 | import asyncFunctionHandler from './asyncFunctionHandler'; 6 | import asyncGeneratorHandler from "./asyncGeneratorHandler"; 7 | import functionHandler from './functionHandler'; 8 | 9 | export default async function entrypointHandler(entrypoint: IEntrypoint, req: Request): Promise { 10 | const handler = entrypoint.handler 11 | 12 | const context = createContext(req, entrypoint.serverPath) 13 | 14 | let res: Response | null = null 15 | await asyncLocalStorage.run(context, async () => { 16 | try { 17 | switch(handler.constructor.name) { 18 | case 'Function': { 19 | res = await functionHandler(context, entrypoint, req) 20 | break 21 | } 22 | 23 | case 'AsyncFunction': { 24 | res = await asyncFunctionHandler(context, req, entrypoint) 25 | break 26 | } 27 | 28 | case 'AsyncGeneratorFunction': { 29 | res = await asyncGeneratorHandler(req, entrypoint) 30 | break 31 | } 32 | 33 | case 'GeneratorFunction': { 34 | res = await asyncGeneratorHandler(req, entrypoint) 35 | break 36 | } 37 | 38 | default: { 39 | res = new Response('Not implemented yet', { status: 422 }) 40 | break 41 | } 42 | } 43 | } catch (err) { 44 | console.error(`There was an error while handling the request ${req.url}:`, err); 45 | res = new Response('Internal server error', { status: 500 }) 46 | // TODO: add observability on this 47 | } 48 | }) 49 | if (!res) { 50 | res = new Response('Something weird happened', { status: 500 }) 51 | } 52 | return res 53 | } -------------------------------------------------------------------------------- /server/handlers/functionHandler.ts: -------------------------------------------------------------------------------- 1 | import IContext from "../../observability/context" 2 | import { Trace } from "../../observability/tracer" 3 | import { IEntrypoint } from "../entrypoints" 4 | 5 | export default async function functionHandler(context: IContext, entrypoint: IEntrypoint, req: Request) { 6 | const handler = entrypoint.handler 7 | let body = {} 8 | if (req.headers.get('content-type')?.includes('application/json')) body = await req.clone().json() 9 | 10 | const tracer = new Trace(context, entrypoint.serverPath, 'entrypoint') 11 | 12 | return tracer.startActiveSpan((span: any) => { 13 | context.span = span 14 | 15 | tracer.addAttribute('entrypoint.request.body', body) 16 | 17 | try { 18 | const result = handler(req) 19 | tracer.addAttribute('entrypoint.response', result) 20 | tracer.end() 21 | if (result instanceof Response) { 22 | return result 23 | } 24 | if (typeof result === 'object') { 25 | // res.json(result) 26 | return new Response(JSON.stringify(result), { 27 | headers: { 28 | 'Content-Type': 'application/json' 29 | } 30 | }) 31 | } 32 | else if (typeof result === 'string') { 33 | // res.send(result + '\n') 34 | return new Response(result + '\n', { headers: { 35 | 'Content-Type': 'text/html' 36 | } }) 37 | } 38 | else { 39 | // res.send(result.toString) 40 | return new Response(result.toString(), { headers: { 41 | 'Content-Type': 'text/html' 42 | } }) 43 | } 44 | } catch (e: any) { 45 | tracer.err(e) 46 | if (e.message) console.error(`[ERROR] ${entrypoint.serverPath}: ${e?.message}`); 47 | else console.error(`[ERROR] ${entrypoint.serverPath}: ${e}`); 48 | // res.status(500).send('Internal Server Error') 49 | return new Response('Internal server error', { status: 500 }) 50 | } 51 | }) 52 | } -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import setup from './setup'; 2 | import express from 'express' 3 | import { getEntrypoints } from './entrypoints'; 4 | import cors from 'cors' 5 | import IContext from '../observability/context'; 6 | import errorhandler from './error-handler'; 7 | import ReasonServer from './server'; 8 | 9 | const app = express() 10 | 11 | app.use(cors()) 12 | app.use(express.json()) 13 | 14 | process.on('unhandledRejection', (reason, promise: any) => { 15 | const symbols = Object.getOwnPropertySymbols(promise); 16 | const kResourceStoreSymbol = symbols.find(symbol => symbol.description === 'kResourceStore'); 17 | 18 | if (reason instanceof SyntaxError && reason.message.includes('REASON__INTERNAL__INFO') && reason.message.includes('The requested module') && reason.message.includes('does not provide an export named')) { 19 | try { 20 | const action = reason.message.split("'")[1] 21 | const agentWithLineNumber = reason.stack?.split('\n')[0] 22 | const agent = agentWithLineNumber?.substring(0, agentWithLineNumber.lastIndexOf(':')) 23 | 24 | console.error(`ReasonError: The action \`${action}\` imported in agent \`${agent}\` is not LLM-callable.\n\nTo make an action LLM-callable, add the prompt information above its declaration by adding the proper JSDoc. For instance: 25 | \`\`\` 26 | /** 27 | * This is a sample description for the foo action 28 | * @param bar This is a sample description for the bar parameter 29 | */ 30 | export default function foo(bar: string) { 31 | // do work 32 | } 33 | \`\`\` 34 | 35 | For more information see link-docs\n\n(Error code: 9910)`) 36 | 37 | return 38 | } catch {} 39 | } 40 | 41 | if (kResourceStoreSymbol) { 42 | const resourceStore = promise[kResourceStoreSymbol] as IContext; 43 | if (resourceStore) { 44 | if (resourceStore.hasErrored) return 45 | return errorhandler(reason, resourceStore) 46 | } 47 | } 48 | 49 | console.error('Unhandled Rejection at:', promise, '\nReason:', reason); 50 | }) 51 | 52 | async function main() { 53 | await setup() 54 | 55 | let port = 1704 56 | if (process.argv.length >= 4 && process.argv[2] === '--port') { 57 | port = parseInt(process.argv[3]) 58 | } 59 | 60 | const entrypoints = await getEntrypoints() 61 | 62 | const server = new ReasonServer(entrypoints) 63 | await server.serve(port) 64 | } 65 | 66 | main() 67 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import url from 'url' 3 | import c from 'ansi-colors' 4 | 5 | import { IEntrypoint } from "./entrypoints"; 6 | import { getRequest, setResponse } from './fetch-standard'; 7 | import entrypointHandler from './handlers/entrypointHandler'; 8 | 9 | type HTTPMethods = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' 10 | 11 | interface Route { 12 | handler: Function; 13 | } 14 | 15 | function isStreamEntrypoint(entrypoint: IEntrypoint) { 16 | return entrypoint.handler.constructor.name.includes('Generator'); 17 | } 18 | 19 | export default class ReasonServer { 20 | entrypoints: IEntrypoint[] 21 | entrypointsRoutes: Record = {} 22 | internalRoutes: Record = {} 23 | 24 | constructor(entrypoints: IEntrypoint[]) { 25 | this.entrypoints = entrypoints 26 | 27 | for (const entrypoint of entrypoints) { 28 | this.entrypointsRoutes[`${entrypoint.method}-${entrypoint.serverPath}`] = entrypoint 29 | console.log(`${c.gray.bold('RΞASON')} — ${c.cyan.italic(entrypoint.method)} ${c.cyan.italic(entrypoint.serverPath)} ${isStreamEntrypoint(entrypoint) ? 'is a streaming entrypoint and was ' : ''}registered...`); 30 | } 31 | 32 | this.setupInternalRoutes() 33 | } 34 | 35 | private setupInternalRoutes() { 36 | this.internalRoutes['GET-/__reason_internal__/entrypoints'] = () => { 37 | return new Response(JSON.stringify(this.entrypoints), { 38 | headers: { 39 | 'Content-Type': 'application/json' 40 | } 41 | }) 42 | } 43 | } 44 | 45 | public async serve(port: number) { 46 | const server = http.createServer(async (req, res) => { 47 | this.handleRequest(req, res) 48 | }) 49 | 50 | server.listen(port, () => { 51 | console.log(`${c.gray.bold('RΞASON')} — ✓ Ready at port ${c.cyan.italic(port.toString())}!`); 52 | }) 53 | } 54 | 55 | private cors(res: Response, origin = '*') { 56 | const clone = res.clone() 57 | 58 | const headers = new Headers(clone.headers) 59 | 60 | headers.set('Access-Control-Allow-Origin', origin) 61 | headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') 62 | headers.set('Access-Control-Allow-Headers', '*') 63 | // { 64 | // 'Access-Control-Allow-Origin': '*', 65 | // 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 66 | // 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 67 | // } 68 | 69 | return new Response(clone.body, { 70 | status: clone.status, 71 | statusText: clone.statusText, 72 | headers: headers 73 | }) 74 | } 75 | 76 | private async handleOPTIONS(req: http.IncomingMessage, res: http.ServerResponse): Promise { 77 | res.setHeader('Access-Control-Allow-Origin', '*'); 78 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); 79 | res.setHeader('Access-Control-Allow-Headers', '*'); 80 | 81 | res.writeHead(204); 82 | res.end(); 83 | } 84 | 85 | private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise { 86 | const parsedUrl = url.parse(req.url!, true) 87 | const path = parsedUrl.pathname ?? '' 88 | const method = req.method?.toUpperCase() ?? '' 89 | 90 | if (method === 'OPTIONS') { 91 | return this.handleOPTIONS(req, res) 92 | } 93 | 94 | const request = await getRequest('http://localhost:1704', req, req.socket.remoteAddress) 95 | let response: Response | null = null 96 | 97 | const entrypoint = this.entrypointsRoutes[`${method}-${path}`] 98 | if (entrypoint) { 99 | response = await this.handleEntrypointRequest(entrypoint, request) 100 | } 101 | 102 | const internalRoute = this.internalRoutes[`${method}-${path}`] 103 | if (internalRoute) { 104 | response = await this.handleInternalRoute(internalRoute, request) 105 | } 106 | 107 | if (response) { 108 | response = this.cors(response) 109 | return await setResponse(res, response) 110 | } 111 | 112 | res.writeHead(404) 113 | res.end() 114 | } 115 | 116 | private async handleInternalRoute(fn: Function, req: Request): Promise { 117 | let response = await fn(req) 118 | if (!response) return new Response('REASON internal route did not return', { status: 500 }) 119 | return response 120 | } 121 | 122 | private async handleEntrypointRequest(entrypoint: IEntrypoint, req: Request): Promise { 123 | return await entrypointHandler(entrypoint, req) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /server/serverError.ts: -------------------------------------------------------------------------------- 1 | import isDebug from "../utils/isDebug"; 2 | 3 | export default class ServerError extends Error { 4 | public name: string; 5 | public description: string; 6 | public code: number; 7 | 8 | constructor(description: string, code: number, debug_info?: any) { 9 | const message = `${description}\nError code: ${code}` 10 | super(message) 11 | 12 | this.name = 'ServerError' 13 | this.description = description 14 | this.code = code 15 | 16 | if (isDebug) { 17 | console.log('RΞASON — INTERNAL DEBUG INFORMATION:'); 18 | console.log(debug_info); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /server/setup.ts: -------------------------------------------------------------------------------- 1 | import reasonConfig from '../configs/reason-config'; 2 | import { Kysely } from 'kysely' 3 | import { db } from "../services/db"; 4 | import setupOpenTEL from '../observability/setup-opentel'; 5 | import isDebug from '../utils/isDebug'; 6 | 7 | export default async function setup() { 8 | if (isDebug) console.log('RΞASON — `.reason.config.js` was sucessfully imported'); 9 | 10 | setupOpenTEL() 11 | 12 | const database = db as Kysely 13 | try { 14 | await database.schema 15 | .createTable('agent_history') 16 | .addColumn('id', 'text', (col) => col.primaryKey()) 17 | .addColumn('messages', 'text', col => col.notNull()) 18 | .execute() 19 | } catch {} 20 | } 21 | -------------------------------------------------------------------------------- /services/db.ts: -------------------------------------------------------------------------------- 1 | import { Kysely } from 'kysely' 2 | import type Database from '../types/db/database.d.ts' 3 | 4 | import sqlite3 from 'sqlite3' 5 | import { SqliteDialect } from '../sqlite/sqlite-dialect' 6 | 7 | const dialect = new SqliteDialect({ 8 | database: new sqlite3.Database('database.sqlite'), 9 | }) 10 | 11 | export const db = new Kysely({ 12 | dialect 13 | }) -------------------------------------------------------------------------------- /sqlite/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/try-reason/reason/113046a45d8cceea6ac6cfe87af77565fdabf06a/sqlite/.DS_Store -------------------------------------------------------------------------------- /sqlite/sqlite-dialect-config.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseConnection } from "kysely"; 2 | 3 | /** 4 | * Config for the SQLite dialect. 5 | */ 6 | export interface SqliteDialectConfig { 7 | /** 8 | * An sqlite Database instance or a function that returns one. 9 | * 10 | * If a function is provided, it's called once when the first query is executed. 11 | * 12 | * https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/api.md#new-databasepath-options 13 | */ 14 | database: SqliteDatabase | (() => Promise); 15 | 16 | /** 17 | * Called once when the first query is executed. 18 | * 19 | * This is a Kysely specific feature and does not come from the `better-sqlite3` module. 20 | */ 21 | onCreateConnection?: (connection: DatabaseConnection) => Promise; 22 | } 23 | 24 | /** 25 | * This interface is the subset of node-sqlite3 driver's `Database` class that 26 | * kysely needs. 27 | */ 28 | export interface SqliteDatabase { 29 | close(callback?: (err: Error | null) => void): void; 30 | prepare( 31 | sql: string, 32 | callback?: (this: SqliteStatement, err: Error | null) => void 33 | ): SqliteStatement; 34 | } 35 | 36 | export interface SqliteStatement { 37 | //readonly reader: boolean; 38 | all( 39 | parameters: ReadonlyArray, 40 | callback?: (this: SqliteRunResult, err: Error | null, rows: any[]) => void 41 | ): this; 42 | 43 | run( 44 | parameters: ReadonlyArray, 45 | callback?: (this: SqliteRunResult, err: Error | null) => void 46 | ): this; 47 | 48 | finalize(callback?: (err: Error | null) => void): void; 49 | } 50 | 51 | export interface SqliteRunResult { 52 | lastID: number; 53 | changes: number; 54 | } 55 | -------------------------------------------------------------------------------- /sqlite/sqlite-dialect.ts: -------------------------------------------------------------------------------- 1 | import { Driver, SqliteQueryCompiler, SqliteIntrospector, SqliteAdapter } from "kysely"; 2 | import { Kysely } from "kysely"; 3 | import { QueryCompiler } from "kysely"; 4 | import { Dialect } from "kysely"; 5 | import { DialectAdapter } from "kysely"; 6 | import { DatabaseIntrospector } from "kysely"; 7 | 8 | import { SqliteDriver } from "./sqlite-driver.js"; 9 | import { SqliteDialectConfig } from "./sqlite-dialect-config.js"; 10 | 11 | /** 12 | * SQLite dialect that uses the [better-sqlite3](https://github.com/JoshuaWise/better-sqlite3) library. 13 | * 14 | * The constructor takes an instance of {@link SqliteDialectConfig}. 15 | * 16 | * ```ts 17 | * import Database from 'better-sqlite3' 18 | * 19 | * new SqliteDialect({ 20 | * database: new Database('db.sqlite') 21 | * }) 22 | * ``` 23 | * 24 | * If you want the pool to only be created once it's first used, `database` 25 | * can be a function: 26 | * 27 | * ```ts 28 | * import Database from 'better-sqlite3' 29 | * 30 | * new SqliteDialect({ 31 | * database: async () => new Database('db.sqlite') 32 | * }) 33 | */ 34 | export class SqliteDialect implements Dialect { 35 | readonly #config: SqliteDialectConfig; 36 | 37 | constructor(config: SqliteDialectConfig) { 38 | this.#config = Object.freeze({ ...config }); 39 | } 40 | 41 | createDriver(): Driver { 42 | return new SqliteDriver(this.#config); 43 | } 44 | 45 | createQueryCompiler(): QueryCompiler { 46 | return new SqliteQueryCompiler(); 47 | } 48 | 49 | createAdapter(): DialectAdapter { 50 | return new SqliteAdapter(); 51 | } 52 | 53 | createIntrospector(db: Kysely): DatabaseIntrospector { 54 | return new SqliteIntrospector(db); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sqlite/sqlite-driver.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseConnection, QueryResult } from "kysely"; 2 | import { Driver } from "kysely"; 3 | import { CompiledQuery } from "kysely"; 4 | import { 5 | SqliteDatabase, 6 | SqliteDialectConfig, 7 | } from "./sqlite-dialect-config.js"; 8 | 9 | export class SqliteDriver implements Driver { 10 | readonly #config: SqliteDialectConfig; 11 | readonly #connectionMutex = new ConnectionMutex(); 12 | 13 | #db?: SqliteDatabase; 14 | #connection?: DatabaseConnection; 15 | 16 | constructor(config: SqliteDialectConfig) { 17 | this.#config = Object.freeze({ ...config }); 18 | } 19 | 20 | async init(): Promise { 21 | this.#db = typeof this.#config.database === 'function' 22 | ? await this.#config.database() 23 | : this.#config.database; 24 | 25 | this.#connection = new SqliteConnection(this.#db); 26 | 27 | if (this.#config.onCreateConnection) { 28 | await this.#config.onCreateConnection(this.#connection); 29 | } 30 | } 31 | 32 | async acquireConnection(): Promise { 33 | // SQLite only has one single connection. We use a mutex here to wait 34 | // until the single connection has been released. 35 | await this.#connectionMutex.lock(); 36 | return this.#connection!; 37 | } 38 | 39 | async beginTransaction(connection: DatabaseConnection): Promise { 40 | await connection.executeQuery(CompiledQuery.raw("begin")); 41 | } 42 | 43 | async commitTransaction(connection: DatabaseConnection): Promise { 44 | await connection.executeQuery(CompiledQuery.raw("commit")); 45 | } 46 | 47 | async rollbackTransaction(connection: DatabaseConnection): Promise { 48 | await connection.executeQuery(CompiledQuery.raw("rollback")); 49 | } 50 | 51 | async releaseConnection(): Promise { 52 | this.#connectionMutex.unlock(); 53 | } 54 | 55 | async destroy(): Promise { 56 | const self = this; 57 | return new Promise((resolve, reject) => { 58 | if (self.#db) { 59 | self.#db.close(function (error) { 60 | if (error) { 61 | reject(error); 62 | } else { 63 | self.#db = undefined; 64 | resolve(); 65 | } 66 | }); 67 | } else { 68 | resolve(); 69 | } 70 | }); 71 | } 72 | } 73 | 74 | class SqliteConnection implements DatabaseConnection { 75 | readonly #db: SqliteDatabase; 76 | 77 | constructor(db: SqliteDatabase) { 78 | this.#db = db; 79 | } 80 | 81 | executeQuery(compiledQuery: CompiledQuery): Promise> { 82 | const { sql, parameters } = compiledQuery; 83 | 84 | return new Promise((resolve, reject) => { 85 | const stmt = this.#db.prepare(sql, function (error) { 86 | if (error) { 87 | reject(error); 88 | } 89 | }); 90 | 91 | if (!stmt) { 92 | reject(new Error("Statement is null")); 93 | return; 94 | } 95 | 96 | stmt.all(parameters, function (error, rows) { 97 | if (error) { 98 | reject(error); 99 | } else { 100 | const changes = this.changes; 101 | const lastInsertRowid = this.lastID; 102 | 103 | stmt.finalize(function (error) { 104 | if (error) { 105 | reject(error); 106 | } 107 | }); 108 | 109 | resolve({ 110 | numUpdatedOrDeletedRows: 111 | changes !== undefined && changes !== null 112 | ? BigInt(changes) 113 | : undefined, 114 | insertId: 115 | lastInsertRowid !== undefined && lastInsertRowid !== null 116 | ? BigInt(lastInsertRowid) 117 | : undefined, 118 | rows: rows ?? [], 119 | }); 120 | } 121 | }); 122 | }); 123 | } 124 | 125 | async *streamQuery(): AsyncIterableIterator> { 126 | throw new Error("Sqlite driver doesn't support streaming"); 127 | } 128 | } 129 | 130 | class ConnectionMutex { 131 | #promise?: Promise; 132 | #resolve?: () => void; 133 | 134 | async lock(): Promise { 135 | while (this.#promise) { 136 | await this.#promise; 137 | } 138 | 139 | this.#promise = new Promise((resolve) => { 140 | this.#resolve = resolve; 141 | }); 142 | } 143 | 144 | unlock(): void { 145 | const resolve = this.#resolve; 146 | 147 | this.#promise = undefined; 148 | this.#resolve = undefined; 149 | 150 | resolve?.(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/server/entrypoints.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { getEntrypoints } from '../../server/entrypoints' 3 | 4 | 5 | test('getEntrypoints', async () => { 6 | const entrypoints = await getEntrypoints() 7 | 8 | const expected = [ 9 | { 10 | prettyName: "index", 11 | serverPath: "/" 12 | }, { 13 | prettyName: "index", 14 | serverPath: "/users/avatar" 15 | }, { 16 | prettyName: "another", 17 | serverPath: "/users/another" 18 | }, { 19 | prettyName: "[id]", 20 | serverPath: "/:id" 21 | }, 22 | { 23 | prettyName: "delete", 24 | serverPath: "/users/:user-id/delete" 25 | } 26 | ] 27 | 28 | for (let e of expected) { 29 | const entrypoint = entrypoints.find(ep => ep.serverPath === e.serverPath) 30 | expect(entrypoint).toBeDefined() 31 | expect(entrypoint!.prettyName).toBe(e.prettyName) 32 | } 33 | }) -------------------------------------------------------------------------------- /tests/utils/complete-json.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'bun:test' 2 | import completeJSON from '../../utils/complete-json' 3 | 4 | test('completeJSON case 1', () => { 5 | let incomplete = `{ 6 | "url_classification": "videogame-related", 7 | "related_sites": [ 8 | { 9 | "name": "GameSpot", 10 | "url": "https://www.gamespot.com", 11 | "brief_description": "GameSpot is a video gaming website that provides news, reviews, previews, downloads, and other information." 12 | }, 13 | { 14 | "name": "K` 15 | 16 | let complete = { 17 | "url_classification": "videogame-related", 18 | "related_sites": [ 19 | { 20 | "name": "GameSpot", 21 | "url": "https://www.gamespot.com", 22 | "brief_description": "GameSpot is a video gaming website that provides news, reviews, previews, downloads, and other information." 23 | }, 24 | { 25 | "name": "K" 26 | } 27 | ] 28 | } 29 | 30 | try { 31 | const data = JSON.parse(completeJSON(incomplete)) 32 | expect(data).toEqual(complete) 33 | } catch (e) { 34 | expect(true).toBe(false) 35 | } 36 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // enable latest features 4 | "lib": ["ESNext"], 5 | // changed from esnext 6 | "module": "ESNext", 7 | "target": "ESNext", 8 | 9 | // my own 10 | "outDir": "dist", 11 | "esModuleInterop": true, 12 | "moduleResolution": "Bundler", 13 | "declaration": true, 14 | 15 | // if TS 5.x+ 16 | // "moduleResolution": "bundler", 17 | // "noEmit": true, 18 | // "allowImportingTsExtensions": true, 19 | "moduleDetection": "force", 20 | 21 | "allowJs": true, // allow importing `.js` from `.ts` 22 | 23 | // // best practices 24 | "strict": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "skipLibCheck": true, 27 | "composite": true, 28 | "downlevelIteration": true, 29 | "allowSyntheticDefaultImports": true 30 | }, 31 | "exclude": [ 32 | "sample", 33 | "web", 34 | "dist", 35 | "tests", 36 | "node_modules" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /types/actionConfig.d.ts: -------------------------------------------------------------------------------- 1 | export default interface ActionConfig { 2 | streamActionUsage?: boolean 3 | } -------------------------------------------------------------------------------- /types/agentConfig.d.ts: -------------------------------------------------------------------------------- 1 | import { OAIChatModels } from "../services/getChatCompletion"; 2 | 3 | export default interface AgentConfig { 4 | model?: OAIChatModels; 5 | temperature?: number; 6 | 7 | /** 8 | * The maximum number of turns this agent will take before stopping. A turn is defined as a response from the LLM model — calling an action or sending a message. 9 | */ 10 | max_turns?: number; 11 | 12 | /** 13 | * If this is `false`, the agent will not automatically stream back to the client the text it receives from the LLM model. This is useful if you want to do something with the text before sending it back to the client. 14 | * 15 | * By default, this is true. 16 | */ 17 | streamText?: boolean; 18 | 19 | /** 20 | * If `true`, RΞASON will send the memory ID of the current conversation automatically to the client as the `memory_id` field in the root of the streamed object. 21 | * This only works if the agent is in stream mode, otherwise the memory ID will not be sent automatically. 22 | * 23 | * The default is `true`. 24 | */ 25 | sendMemoryID?: boolean; 26 | 27 | /** 28 | * If `true`, RΞASON won't automatically save the history of the current conversation. If `true` passing a `memory_id` to `useAgent` will do nothing and `memory_id` will also not be sent. 29 | * 30 | * The default is `false`. 31 | */ 32 | disableAutoSave?: boolean; 33 | } -------------------------------------------------------------------------------- /types/db/agent-history.d.ts: -------------------------------------------------------------------------------- 1 | export default interface AgentHistory { 2 | id: string; 3 | messages: string; 4 | } -------------------------------------------------------------------------------- /types/db/database.d.ts: -------------------------------------------------------------------------------- 1 | import AgentHistory from "./agent-history.d.ts"; 2 | 3 | export default interface Database { 4 | agent_history: AgentHistory 5 | } -------------------------------------------------------------------------------- /types/iagent.d.ts: -------------------------------------------------------------------------------- 1 | export interface Action { 2 | name: string; 3 | input: Record; 4 | output: any; 5 | } 6 | 7 | export interface LLMTextReturn { 8 | content: string; 9 | done: boolean; 10 | } 11 | 12 | export interface UserMessage { 13 | role: 'user' | 'system' 14 | content: string 15 | } 16 | 17 | export interface AssistantMessageText { 18 | role: 'assistant'; 19 | content: string; 20 | } 21 | 22 | export interface FunctionResponseMessage { 23 | role: 'tool' 24 | tool_call_id: string; 25 | name: string 26 | content: string 27 | } 28 | 29 | export interface AssistantMessageAction { 30 | role: 'assistant'; 31 | content: null; 32 | function_call: { 33 | name: string; 34 | arguments: string; 35 | } 36 | } 37 | 38 | type Message = UserMessage | AssistantMessageText | AssistantMessageAction | FunctionResponseMessage 39 | 40 | export interface IMessages { 41 | functions?: { 42 | name: string; 43 | description: string; 44 | parameters: { 45 | type: 'object'; 46 | required: string[] 47 | properties: Record 48 | } 49 | }[]; 50 | function_call?: { 51 | name: string; 52 | } 53 | 54 | messages: Message[] 55 | } 56 | 57 | export { Message } 58 | 59 | export interface Prompt extends IMessage, OAIOptions { 60 | model: string; 61 | } 62 | 63 | export interface ReasonActionReturn { 64 | message: null; 65 | } 66 | 67 | export interface ReasonTextReturn { 68 | actions: null; 69 | message: LLMTextReturn; 70 | } 71 | 72 | export interface ReasonActionAndTextReturn { 73 | actions: Action[]; 74 | message: LLMTextReturn; 75 | } 76 | 77 | export { ReasonTextReturn, ReasonActionReturn } 78 | 79 | export default interface Agent { 80 | reason(prompt: string, state?: any): AsyncGenerator 81 | run(prompt: string, state?: any): Promise 82 | stop(): void 83 | messages: { 84 | next(message: string): void 85 | getID(): string; 86 | get(): Message[]; 87 | set(messages: Message[]): void; 88 | } 89 | } -------------------------------------------------------------------------------- /types/oai-chat-models.d.ts: -------------------------------------------------------------------------------- 1 | type OAIChatModels = 'gpt-3.5-turbo' | 'gpt-4' | 'gpt-3.5-turbo-16k' | 'gpt-4-1106-preview' | 'gpt-4-turbo-preview' 2 | 3 | export default OAIChatModels -------------------------------------------------------------------------------- /types/reasonConfig.d.ts: -------------------------------------------------------------------------------- 1 | import { SpanExporter } from '@opentelemetry/sdk-trace-base'; 2 | import OAIChatModels from './oai-chat-models' 3 | 4 | export default interface ReasonConfig { 5 | projectName: string; 6 | 7 | openai: { 8 | key: string; 9 | defaultModel: OAIChatModels; 10 | }; 11 | 12 | anyscale: { 13 | key: string; 14 | defaultModel: string; 15 | } 16 | 17 | openTelemetryExporter: SpanExporter; 18 | } -------------------------------------------------------------------------------- /types/server.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express"; 2 | 3 | type ReasonRequest = Request 4 | 5 | export { ReasonRequest } -------------------------------------------------------------------------------- /types/streamable.d.ts: -------------------------------------------------------------------------------- 1 | interface StreamableDone { 2 | /** 3 | * This indicates if this value has been fully streamed by the LLM. 4 | * 5 | * If `false` then this value has not been fully streamed by the LLM — either because it has not even started or because it is being streaming right now but not fully finished. 6 | */ 7 | done: true; 8 | 9 | /** 10 | * This is the actual value of this parameter. 11 | * 12 | * If `null` then this value has not even started to be streamed by the LLM. 13 | * 14 | * You can use this to check if the LLM has started to stream this value (if `value` != `null` && `done` == `false` then this is the value that is currently being streamed). 15 | */ 16 | value: T; 17 | 18 | /** 19 | * This is a property that won't be streamed allowing you to store intermediary values that you don't want to stream back to your client. 20 | * 21 | * It is an object that you can add whatever properties you'd like. 22 | */ 23 | internal: Record; 24 | } 25 | 26 | interface StreamableNotDone { 27 | /** 28 | * This indicates if this value has been fully streamed by the LLM. 29 | * 30 | * If `false` then this value has not been fully streamed by the LLM — either because it has not even started or because it is being streaming right now but not fully finished. 31 | */ 32 | done: false; 33 | 34 | /** 35 | * This is the actual value of this parameter. 36 | * 37 | * If `null` then this value has not even started to be streamed by the LLM. 38 | * 39 | * You can use this to check if the LLM has started to stream this value (if `value` != `null` && `done` == `false` then this is the value that is currently being streamed). 40 | */ 41 | value: Partial | null; 42 | 43 | /** 44 | * This is a property that won't be streamed allowing you to store intermediary values that you don't want to stream back to your client. 45 | * 46 | * It is an object that you can add whatever properties you'd like — and the data in it will get persisted throughout all iterations of `reasonStream()`. 47 | */ 48 | internal: Record; 49 | } 50 | 51 | type Streamable = StreamableDone | StreamableNotDone; 52 | 53 | export { StreamableDone, StreamableNotDone } 54 | 55 | export default Streamable -------------------------------------------------------------------------------- /types/thinkConfig.d.ts: -------------------------------------------------------------------------------- 1 | import { OAIChatModels } from '../services/getChatCompletion'; 2 | 3 | interface ThinkConfig { 4 | model?: OAIChatModels; 5 | validation_strategy?: 'ignore' | 'error' //| 'retry'; 6 | temperature?: number; 7 | max_tokens?: number; 8 | autoStream?: boolean; 9 | } 10 | 11 | interface OAIMessage { 12 | role: 'system' | 'user' | 'assistant'; 13 | content: string; 14 | } 15 | 16 | export { ThinkConfig, OAIMessage } -------------------------------------------------------------------------------- /utils/asyncLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from 'async_hooks'; 2 | 3 | const asyncLocalStorage = new AsyncLocalStorage(); 4 | 5 | export default asyncLocalStorage -------------------------------------------------------------------------------- /utils/isDebug.js: -------------------------------------------------------------------------------- 1 | let reasonInternalDebug = process.env?.REASON_INTERNAL_DEBUG 2 | 3 | function isWindows() { 4 | return process.platform === 'win32' 5 | } 6 | 7 | if (isWindows() && reasonInternalDebug) { 8 | reasonInternalDebug = reasonInternalDebug.trim() 9 | } 10 | 11 | const isDebug = reasonInternalDebug === 'true'; 12 | export default isDebug; 13 | -------------------------------------------------------------------------------- /utils/isDebug.ts: -------------------------------------------------------------------------------- 1 | let reasonInternalDebug = process.env?.REASON_INTERNAL_DEBUG 2 | 3 | function isWindows() { 4 | return process.platform === 'win32' 5 | } 6 | 7 | if (isWindows() && reasonInternalDebug) { 8 | reasonInternalDebug = reasonInternalDebug.trim() 9 | } 10 | 11 | const isDebug = reasonInternalDebug === 'true'; 12 | export default isDebug; 13 | -------------------------------------------------------------------------------- /utils/libname.js: -------------------------------------------------------------------------------- 1 | const libname = 'tryreason' 2 | 3 | export { libname } -------------------------------------------------------------------------------- /utils/oai-function.ts: -------------------------------------------------------------------------------- 1 | import Ajv from "ajv" 2 | import { ChatResponseFunction, OAIFunction } from "../services/getChatCompletion" 3 | import ReasonError from "./reasonError.js" 4 | import isDebug from "./isDebug" 5 | import { OAITool, ToolResponse } from "../services/openai/getChatCompletion" 6 | 7 | interface ReasonActionInfo { 8 | name: string 9 | prompt: string 10 | parameters: { 11 | name: string 12 | type: string 13 | required: boolean 14 | prompt?: string 15 | }[] 16 | } 17 | 18 | export { ReasonActionInfo } 19 | 20 | export default function action2OAIfunction(action: ReasonActionInfo): OAITool { 21 | let properties: OAIFunction['parameters']['properties'] = {} 22 | let required: OAIFunction['parameters']['required'] = [] 23 | 24 | let fn: OAITool = { 25 | type: 'function', 26 | function: { 27 | name: action.name, 28 | description: action.prompt, 29 | parameters: { 30 | type: 'object', 31 | required: [], 32 | properties: {} 33 | } 34 | } 35 | } 36 | 37 | for (let param of action.parameters) { 38 | const name = param.name 39 | 40 | if (param.required) required.push(name) 41 | 42 | if (properties[name]) throw new ReasonError(`During a typed \`reason()\` call you defined the same property \`${name}\` more than once. This is not allowed.`, 201) 43 | 44 | properties[name] = JSON.parse(param.type) 45 | if (param.prompt) properties[name].description = param.prompt 46 | } 47 | 48 | fn.function.parameters.properties = properties 49 | fn.function.parameters.required = required 50 | 51 | return fn 52 | } 53 | // [ 54 | // { 55 | // name: 'classification', 56 | // type: '{"enum":["videogame-related","news","social-media"]}', 57 | // prompt: 'the webpage classification. you should classify to one the categories as close as possible', 58 | // required: true 59 | // } 60 | // ] 61 | const AAjv = Ajv as any 62 | const ajv = new AAjv(); 63 | 64 | function removeAllNullProperties(obj: Record) { 65 | for (let key of Object.keys(obj)) { 66 | if (obj[key] === null) 67 | delete obj[key]; 68 | if (typeof obj[key] === 'object') 69 | removeAllNullProperties(obj[key]); 70 | } 71 | return obj; 72 | } 73 | 74 | export function validateOAIfunction(completion: ToolResponse, actions: ReasonActionInfo[]): any { 75 | const fn = actions.find(action => action.name === completion.function.name); 76 | 77 | if (!fn) throw new ReasonError(`OpenAI returned a function that does not exist in the functions they were passed.`, 602); 78 | 79 | const parsedArguments = JSON.parse(completion.function.arguments); 80 | 81 | try { 82 | removeAllNullProperties(parsedArguments) 83 | } catch(err: any) { 84 | if (isDebug) { 85 | console.error('Error occurred while removing null properties (Err code: 8281)') 86 | console.error(err) 87 | } 88 | } 89 | 90 | for (let parameter of fn.parameters) { 91 | if (parameter.required && !(parameter.name in parsedArguments)) { 92 | throw new ReasonError(`The parameter \`${parameter.name}\` was not returned from the LLM but you marked it as required.`, 603); 93 | } 94 | 95 | // Validate against JSON Schema type 96 | const isValid = ajv.validate(JSON.parse(parameter.type), parsedArguments[parameter.name]); 97 | 98 | if (isDebug) { 99 | console.log('parameter from code'); 100 | console.log(parameter); 101 | console.log('parameter from oai'); 102 | console.log(parsedArguments[parameter.name]); 103 | } 104 | 105 | if (!isValid) { 106 | throw new ReasonError(`You specified the parameter \`${parameter.name}\` to be of type ${parameter.type} but the LLM returned "${parsedArguments[parameter.name]}".`, 604, { parameter: parameter.name, type: parameter.type, value: parsedArguments[parameter.name] }); 107 | } 108 | } 109 | 110 | return true; 111 | } 112 | -------------------------------------------------------------------------------- /utils/reasonError.js: -------------------------------------------------------------------------------- 1 | export default class ReasonError extends Error { 2 | constructor(message, code, debug_info = {}) { 3 | super(`${message} (Error code ${code})`); 4 | this.code = code; 5 | this.debug_info = debug_info; 6 | } 7 | } -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/try-reason/reason/113046a45d8cceea6ac6cfe87af77565fdabf06a/web/README.md -------------------------------------------------------------------------------- /web/a.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/try-reason/reason/113046a45d8cceea6ac6cfe87af77565fdabf06a/web/a.ts -------------------------------------------------------------------------------- /web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/try-reason/reason/113046a45d8cceea6ac6cfe87af77565fdabf06a/web/app/favicon.ico -------------------------------------------------------------------------------- /web/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | span.property { 70 | color: cyan !important 71 | } 72 | span.string { 73 | color: gray !important 74 | } 75 | 76 | @layer base { 77 | * { 78 | @apply border-border; 79 | } 80 | body { 81 | @apply bg-background text-foreground; 82 | } 83 | } -------------------------------------------------------------------------------- /web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import { Toaster } from '@/components/ui/toaster' 3 | import './globals.css' 4 | import type { Metadata } from 'next' 5 | import { Inter } from 'next/font/google' 6 | import GradientHeading from '@/components/GradientHeading' 7 | import Balancer from 'react-wrap-balancer' 8 | import { ExternalLink } from 'lucide-react' 9 | import Link from 'next/link' 10 | import ReasonTitle from '@/components/ReasonTitle' 11 | 12 | const inter = Inter({ subsets: ['latin'] }) 13 | 14 | export const metadata: Metadata = { 15 | title: 'RΞASON Playground', 16 | description: 'RΞASON playground', 17 | } 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: { 22 | children: React.ReactNode 23 | }) { 24 | function goToMainPage() { 25 | let currentUrl = new URL(window.location.href); 26 | let searchParams = currentUrl.searchParams; 27 | const url = searchParams.get('url'); 28 | 29 | if (url) window.location.href = `/?url=${url}` 30 | else window.location.href = '/' 31 | } 32 | 33 | return ( 34 | 35 | 36 |
37 | 38 | 39 | 42 | 43 | DOCS 44 | 45 |
46 | 47 |
48 | {children} 49 |
50 | 51 | 52 | 53 |
54 |
55 | Background image 64 |
65 |
66 | 67 | 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /web/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import GradientHeading from "@/components/GradientHeading"; 4 | import Balancer from 'react-wrap-balancer' 5 | import { Button } from "../components/ui/button"; 6 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card"; 7 | import { ChevronRight, Loader2, RotateCcw, RotateCw } from "lucide-react" 8 | import Link from "next/link"; 9 | import { useEffect, useState } from "react"; 10 | import ReasonNotFound from "@/components/ReasonNotFound"; 11 | 12 | interface Entrypoint { 13 | prettyName: string; 14 | serverPath: string; 15 | method: string; 16 | } 17 | 18 | export default function Home({ params, searchParams }: {params: { slug: string }; searchParams: Record}) { 19 | const [entrypoints, setEntrypoints] = useState(null) 20 | 21 | const [newUrl, setNewUrl] = useState('') 22 | const [isLoading, setIsLoading] = useState(false) 23 | 24 | function getReasonURL() { 25 | if (newUrl !== '') { 26 | return newUrl 27 | } 28 | 29 | if (searchParams?.url) { 30 | return searchParams.url 31 | } 32 | 33 | return 'http://localhost:1704' 34 | } 35 | 36 | async function fetchEntrypoints(url?: string) { 37 | setIsLoading(true) 38 | try { 39 | const res = await fetch(`${url ? url : getReasonURL()}/__reason_internal__/entrypoints`, { 40 | method: 'GET', 41 | cache: 'no-cache' 42 | }) 43 | 44 | if (res.ok) { 45 | const data = await res.json() 46 | setEntrypoints(data) 47 | return data 48 | } else { 49 | setEntrypoints('not-connected') 50 | return null 51 | } 52 | } catch { 53 | setEntrypoints('not-connected') 54 | return null 55 | } finally { 56 | setIsLoading(false) 57 | } 58 | } 59 | 60 | useEffect(() => { 61 | fetchEntrypoints() 62 | }, []) 63 | 64 | return ( 65 |
66 | {entrypoints === null && ( 67 |
68 | 69 | Connecting to your RΞASON project... 70 |
71 | )} 72 | 73 | {entrypoints === 'not-connected' && ( 74 | 75 | )} 76 | 77 | {Array.isArray(entrypoints) && ( 78 | <> 79 | Entrypoints 80 | Learn more about entrypoints here. 81 | 82 |
83 | {entrypoints.map((entrypoint) => ( 84 | 85 | 86 |
87 | {entrypoint.prettyName} 88 | {entrypoint.method} {entrypoint.serverPath} 89 |
90 | 91 | 92 | 93 | 94 |
95 |
96 | ))} 97 |
98 | 99 | )} 100 |
101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /web/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/try-reason/reason/113046a45d8cceea6ac6cfe87af77565fdabf06a/web/bun.lockb -------------------------------------------------------------------------------- /web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /web/components/GradientHeading.tsx: -------------------------------------------------------------------------------- 1 | import gradients from '@/gradients'; 2 | 3 | interface ColorsProps { 4 | color1: string; 5 | color2: string; 6 | direction?: string; 7 | } 8 | 9 | interface GradType { 10 | type: keyof typeof gradients; 11 | } 12 | 13 | type Props = {className?: string; children?: any} & (ColorsProps | GradType); 14 | 15 | export default function GradientHeading(prop: Props) { 16 | if ('type' in prop) { 17 | const { type, ...props } = prop; 18 | 19 | return ( 20 |

24 | {props.children} 25 |

26 | ) 27 | } else { 28 | const { color1, color2, direction = 'to top', ...props } = prop; 29 | return ( 30 |

34 | {props.children} 35 |

36 | ) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /web/components/ReasonTitle/dynamic.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from "next/link"; 4 | import GradientHeading from "../GradientHeading"; 5 | 6 | export default function DynamicReasonTitle() { 7 | function goToMainPage() { 8 | let currentUrl = new URL(window.location.href); 9 | let searchParams = currentUrl.searchParams; 10 | const url = searchParams.get('url'); 11 | 12 | if (url) window.location.href = `/?url=${url}` 13 | else window.location.href = '/' 14 | } 15 | 16 | return ( 17 |
18 | 22 | RΞASON Playground 23 | 24 |
25 | ) 26 | } -------------------------------------------------------------------------------- /web/components/ReasonTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import DynamicReasonTitle from "./dynamic"; 3 | import StaticReasonTitle from "./static"; 4 | 5 | export default function ReasonTitle() { 6 | return ( 7 | }> 8 | 9 | 10 | ) 11 | } -------------------------------------------------------------------------------- /web/components/ReasonTitle/static.tsx: -------------------------------------------------------------------------------- 1 | import GradientHeading from "../GradientHeading"; 2 | 3 | export default function StaticReasonTitle() { 4 | return ( 5 | 9 | RΞASON Playground 10 | 11 | ) 12 | } -------------------------------------------------------------------------------- /web/components/entrypoint/ResponseVisualizer.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import "@uiw/react-textarea-code-editor/dist.css"; 3 | 4 | const CodeEditor = dynamic( 5 | () => import("@uiw/react-textarea-code-editor").then((mod) => mod.default), 6 | { 7 | ssr: false, 8 | loading: () => ( 9 |
10 | ) 11 | }, 12 | ); 13 | 14 | interface Props { 15 | data: any; 16 | } 17 | 18 | export default function ResponseVisualizer({ data }: Props) { 19 | return ( 20 |
21 | {!CodeEditor &&
Loading...
} 22 | {}} 25 | disabled 26 | language="json" 27 | className="abc" 28 | padding={15} 29 | placeholder="The response will appear here" 30 | style={{ 31 | fontSize: 14, 32 | height: "100%", 33 | maxHeight: "100%", 34 | background: "linear-gradient(to bottom right, #101010 0%, #000000 100%)", 35 | fontFamily: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", 36 | borderRadius: '0.5rem', 37 | border: "1px solid rgba(255, 255, 255, 0.1)" 38 | }} 39 | /> 40 |
41 | ) 42 | } -------------------------------------------------------------------------------- /web/components/entrypoint/chat/ActionVisualizer.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion' 2 | import { Check, FunctionSquare, Loader, Loader2 } from "lucide-react"; 3 | import { ReasonStepAction } from "../../../hooks/useChat"; 4 | import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../../ui/accordion"; 5 | 6 | interface Props { 7 | action: Partial; 8 | isExecuting: boolean; 9 | } 10 | 11 | export default function ActionVisualizer({ action, isExecuting }: Props) { 12 | return ( 13 | 18 | 21 | 22 | 23 |
24 | {isExecuting && } 25 | {!isExecuting && } 26 |
27 | {isExecuting ? 'Executing' : 'Executed'} 28 | {action.name} 29 |
30 |
31 |
32 | 33 |

INPUT

34 |
{JSON.stringify(action.input, null, 2)}
35 | 36 |

OUTPUT

37 |
{JSON.stringify(action.output, null, 2)}
38 |
39 |
40 |
41 |
42 | ) 43 | } -------------------------------------------------------------------------------- /web/components/entrypoint/request-information/Body.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import "@uiw/react-textarea-code-editor/dist.css"; 3 | import { useEffect } from "react"; 4 | 5 | const CodeEditor = dynamic( 6 | () => import("@uiw/react-textarea-code-editor").then((mod) => mod.default), 7 | { 8 | ssr: false, 9 | loading: () => ( 10 |
11 | ) 12 | }, 13 | ); 14 | 15 | interface Props { 16 | body: string; 17 | setBody: (body: string) => void; 18 | } 19 | 20 | export default function Body({ body, setBody }: Props) { 21 | console.log(CodeEditor) 22 | return ( 23 |
24 | {!CodeEditor &&
Loading...
} 25 | setBody(e.target.value)} 28 | language="json" 29 | className="abc" 30 | padding={15} 31 | placeholder="Enter JSON body here..." 32 | style={{ 33 | fontSize: 14, 34 | height: "100%", 35 | maxHeight: "100%", 36 | background: "linear-gradient(to bottom right, #101010 0%, #000000 100%)", 37 | fontFamily: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", 38 | borderRadius: '0.5rem', 39 | border: "1px solid rgba(255, 255, 255, 0.1)" 40 | }} 41 | /> 42 |
43 | ) 44 | } -------------------------------------------------------------------------------- /web/components/entrypoint/request-information/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Input } from "@/components/ui/input"; 3 | import { Delete, Plus, X } from "lucide-react"; 4 | 5 | interface Props { 6 | headers: string[][]; 7 | setHeaders: (headers: string[][]) => void; 8 | } 9 | 10 | export default function Header({ headers, setHeaders }: Props) { 11 | function addItem() { 12 | let temp = [...headers] 13 | temp.push(['', '']) 14 | setHeaders(temp) 15 | } 16 | 17 | function removeItem(idx: number) { 18 | let temp = [...headers] 19 | 20 | if (headers.length === 1) { 21 | temp[0] = ['', ''] 22 | setHeaders(temp) 23 | return 24 | } 25 | 26 | temp.splice(idx, 1) 27 | setHeaders(temp) 28 | } 29 | 30 | function changeItem(idx: number, key: string, value: string) { 31 | let temp = [...headers] 32 | temp[idx] = [key, value] 33 | setHeaders(temp) 34 | } 35 | 36 | return ( 37 |
38 | Key 39 | Value 40 |
41 | 42 | {headers.map(([key, value], idx) => ( 43 | <> 44 | changeItem(idx, e.target.value, value)} /> 45 | changeItem(idx, key, e.target.value)} /> 46 | 47 | 48 | ))} 49 | 50 | 53 |
54 | ) 55 | } -------------------------------------------------------------------------------- /web/components/entrypoint/request-information/RequestInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; 2 | import Body from "./Body"; 3 | import Header from "./Header"; 4 | 5 | interface Props { 6 | body: string; 7 | setBody: (body: string) => void; 8 | 9 | headers: string[][]; 10 | setHeaders: (headers: string[][]) => void; 11 | } 12 | 13 | export default function RequestInfo({ body, setBody, headers, setHeaders }: Props) { 14 | return ( 15 |
16 | 17 |
18 | 19 | Headers 20 | Body 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
34 |
35 |
36 |
37 |
38 |
39 | ) 40 | } -------------------------------------------------------------------------------- /web/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "../../lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 55 |
{children}
56 |
57 | )) 58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 59 | 60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 61 | -------------------------------------------------------------------------------- /web/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "../../lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-[linear-gradient(to_bottom,_white_0%,_#999999_100%)] text-primary-foreground hover:brightness-[0.8] transition-all duration-150 ease-out hover:text-black border-[rgba(60,60,60,1)] border border-solid", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-[rgba(255,255,255,0.15)] bg-[linear-gradient(to_top,_#151515_0%,_black_100%)] hover:bg-accent text-zinc-400 hover:text-zinc-200 hover:border-[rgba(255,255,255,0.3)]'", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /web/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "../../lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /web/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "../../lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /web/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "../../lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /web/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /web/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitives from "@radix-ui/react-switch" 5 | 6 | import { cn } from "../../lib/utils" 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )) 27 | Switch.displayName = SwitchPrimitives.Root.displayName 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /web/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 25 | )) 26 | TabsList.displayName = TabsPrimitive.List.displayName 27 | 28 | const TabsTrigger = React.forwardRef< 29 | React.ElementRef, 30 | React.ComponentPropsWithoutRef 31 | >(({ className, ...props }, ref) => ( 32 | 40 | )) 41 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 42 | 43 | const TabsContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | TabsContent.displayName = TabsPrimitive.Content.displayName 57 | 58 | export { Tabs, TabsList, TabsTrigger, TabsContent } 59 | -------------------------------------------------------------------------------- /web/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "../../lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |