├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── ai-bin └── test-agent.sh ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── concepts │ ├── agents.md │ ├── function-composition.md │ ├── index.md │ └── using-the-api.md ├── docs │ └── .gitignore ├── docusaurus.config.js ├── old │ └── actions.md ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── docusaurus-social-card.jpg │ │ ├── docusaurus.png │ │ ├── favicon.ico │ │ ├── logo.svg │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg ├── tsconfig.json └── tutorial │ └── wikipedia-agent │ ├── add-agent.md │ ├── complete-agent.md │ ├── create-agent-ts.md │ ├── create-read-article-tool.md │ ├── create-search-tool.md │ ├── index.md │ ├── setup-actions.md │ ├── setup-llm-model.md │ └── setup-project.md ├── examples ├── babyagi │ ├── README.md │ ├── agent │ │ └── babyagi │ │ │ └── agent.ts │ ├── package.json │ └── tsconfig.json ├── javascript-developer │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── bin │ │ ├── build-executor.sh │ │ └── run-executor.sh │ ├── example │ │ ├── cipher │ │ │ └── task.txt │ │ ├── helloworld │ │ │ ├── task-modify.txt │ │ │ ├── task-typescript.txt │ │ │ └── task.txt │ │ └── roman-numbers │ │ │ ├── task-add-tests.txt │ │ │ └── task.txt │ ├── executor.mjs │ ├── package.json │ ├── screenshot │ │ └── autodev-001.png │ ├── src │ │ ├── main.ts │ │ └── runDeveloperAgent.ts │ └── tsconfig.json ├── pdf-to-twitter-thread │ ├── README.md │ ├── package.json │ ├── src │ │ ├── createTwitterThreadFromPdf.ts │ │ └── main.ts │ └── tsconfig.json ├── split-and-embed-text │ ├── package.json │ ├── src │ │ ├── main.ts │ │ └── splitAndEmbedText.ts │ └── tsconfig.json └── wikipedia │ ├── README.md │ ├── package.json │ ├── screenshot │ └── wikipedia-qa-001.png │ ├── src │ ├── main.ts │ └── runWikipediaAgent.ts │ └── tsconfig.json ├── package.json ├── packages └── agent │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── project.json │ ├── src │ ├── action │ │ ├── Action.ts │ │ ├── ActionParameters.ts │ │ ├── ActionRegistry.ts │ │ ├── ExecuteBasicToolFunction.ts │ │ ├── ExecuteReflectiveToolFunction.ts │ │ ├── FormatResultFunction.ts │ │ ├── done.ts │ │ ├── format │ │ │ ├── ActionFormat.ts │ │ │ ├── flexibleJson.ts │ │ │ ├── index.ts │ │ │ └── json.ts │ │ └── index.ts │ ├── agent │ │ ├── EmbedCall.ts │ │ ├── GenerateCall.ts │ │ ├── Run.ts │ │ ├── RunContext.ts │ │ ├── calculateRunCostInMillicent.ts │ │ ├── controller │ │ │ ├── RunController.ts │ │ │ ├── all.ts │ │ │ ├── cancellable.ts │ │ │ ├── index.ts │ │ │ ├── maxSteps.ts │ │ │ └── noLimit.ts │ │ ├── env │ │ │ ├── LoadEnvironmentKeyFunction.ts │ │ │ ├── index.ts │ │ │ ├── loadEnvironment.ts │ │ │ └── property.ts │ │ ├── index.ts │ │ ├── observer │ │ │ ├── RunObserver.ts │ │ │ ├── combineObservers.ts │ │ │ ├── index.ts │ │ │ └── showRunInConsole.ts │ │ └── runAgent.ts │ ├── convert │ │ ├── htmlToText.ts │ │ ├── index.ts │ │ └── pdfToText.ts │ ├── embedding │ │ ├── EmbedFunction.ts │ │ ├── EmbeddingModel.ts │ │ ├── embed.ts │ │ └── index.ts │ ├── index.ts │ ├── prompt │ │ ├── FormatSectionFunction.ts │ │ ├── Prompt.ts │ │ ├── Section.ts │ │ ├── availableActionsPrompt.ts │ │ ├── chatPromptFromTextPrompt.ts │ │ ├── concatChatPrompts.ts │ │ ├── extractPrompt.ts │ │ ├── formatSectionAsMarkdown.ts │ │ ├── index.ts │ │ ├── recentStepsChatPrompt.ts │ │ ├── rewritePrompt.ts │ │ └── sectionsPrompt.ts │ ├── provider │ │ ├── index.ts │ │ └── openai │ │ │ ├── api │ │ │ ├── OpenAIChatCompletion.ts │ │ │ ├── OpenAIEmbedding.ts │ │ │ ├── OpenAIError.ts │ │ │ ├── OpenAITextCompletion.ts │ │ │ ├── generateChatCompletion.ts │ │ │ ├── generateEmbedding.ts │ │ │ ├── generateTextCompletion.ts │ │ │ └── index.ts │ │ │ ├── chatModel.ts │ │ │ ├── cost │ │ │ ├── calculateChatCompletionCostInMillicent.ts │ │ │ ├── calculateEmbeddingCostInMillicent.ts │ │ │ ├── calculateOpenAICallCostInMillicent.ts │ │ │ ├── calculateTextCompletionCostInMillicent.ts │ │ │ └── index.ts │ │ │ ├── embeddingModel.ts │ │ │ ├── index.ts │ │ │ ├── textModel.ts │ │ │ └── tokenizer.ts │ ├── server │ │ ├── AgentPlugin.ts │ │ ├── ServerAgent.ts │ │ ├── ServerAgentSpecification.ts │ │ ├── index.ts │ │ └── startAgentServer.ts │ ├── source │ │ ├── fileAsArrayBuffer.ts │ │ ├── index.ts │ │ └── webpageAsHtmlText.ts │ ├── step │ │ ├── ErrorStep.ts │ │ ├── FixedStepsLoop.ts │ │ ├── GenerateChatCompletionFunction.ts │ │ ├── GenerateNextStepLoop.ts │ │ ├── Loop.ts │ │ ├── NoopStep.ts │ │ ├── PromptStep.ts │ │ ├── Step.ts │ │ ├── StepFactory.ts │ │ ├── StepResult.ts │ │ ├── StepState.ts │ │ ├── UpdateTasksLoop.ts │ │ ├── createActionStep.ts │ │ └── index.ts │ ├── text-store │ │ ├── InMemoryTextStore.ts │ │ └── index.ts │ ├── text │ │ ├── extract │ │ │ ├── ExtractFunction.ts │ │ │ ├── extractRecursively.ts │ │ │ ├── index.ts │ │ │ └── splitExtractRewrite.ts │ │ ├── generate │ │ │ ├── GeneratorModel.ts │ │ │ ├── generate.ts │ │ │ └── generateText.ts │ │ ├── index.ts │ │ ├── load.ts │ │ └── split │ │ │ ├── SplitFunction.ts │ │ │ ├── index.ts │ │ │ └── splitRecursively.ts │ ├── tokenizer │ │ ├── Tokenizer.ts │ │ └── index.ts │ ├── tool │ │ ├── BasicToolStep.ts │ │ ├── ReflectiveToolStep.ts │ │ ├── ask-user │ │ │ └── AskUserTool.ts │ │ ├── executeRemoteTool.ts │ │ ├── executor │ │ │ ├── ToolRegistry.ts │ │ │ ├── index.ts │ │ │ ├── runToolExecutor.ts │ │ │ └── toolPlugin.ts │ │ ├── extract-information-from-webpage │ │ │ └── ExtractInformationFromWebpageTool.ts │ │ ├── index.ts │ │ ├── programmable-google-search-engine │ │ │ └── ProgrammableGoogleSearchEngineTool.ts │ │ ├── read-file │ │ │ └── ReadFileTool.ts │ │ ├── run-command │ │ │ └── RunCommandTool.ts │ │ ├── update-run-string-property │ │ │ └── UpdateRunStringPropertyTool.ts │ │ └── write-file │ │ │ └── WriteFileTool.ts │ └── util │ │ ├── RetryFunction.ts │ │ ├── cosineSimilarity.ts │ │ ├── createNextId.test.ts │ │ ├── createNextId.ts │ │ ├── gracefullyShutdownOnSigTermAndSigInt.ts │ │ ├── index.ts │ │ └── retryWithExponentialBackoff.ts │ └── tsconfig.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | .DS_Store 3 | .env 4 | node_modules 5 | repository/** -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "cSpell.words": [ 4 | "Backoff", 5 | "concat", 6 | "davinci", 7 | "Fastify", 8 | "gptagent", 9 | "hyperid", 10 | "npmjs", 11 | "openai", 12 | "pino", 13 | "reprioritizing", 14 | "Tiktoken" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lars Grammel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ai-bin/test-agent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Intended to be used by the AI agent (to produce better outputs). 4 | # Needs to be invoked from root of the project. 5 | 6 | cd packages/agent 7 | pnpm jest --no-colors src/ -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/concepts/agents.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Agents 6 | 7 | ## What is an agent? 8 | 9 | An agent flexibly solves a user's task by using large language models (LLM), memory (embeddings), and tools (e.g., search, analyzing data, etc.). 10 | 11 | A basic agent works like this: 12 | 13 | ```mermaid 14 | graph LR; 15 | 16 | TASK["task"]; 17 | CALL_LLM["call LLM"]; 18 | USE_TOOL["use tool"]; 19 | DONE["done"]; 20 | 21 | TASK-->CALL_LLM; 22 | CALL_LLM-->USE_TOOL; 23 | CALL_LLM-->DONE; 24 | USE_TOOL-->CALL_LLM; 25 | ``` 26 | 27 | The critical piece is that **the language model response determines what tool to use**. 28 | This enables the agent to be flexible and solve a wide variety of tasks. 29 | 30 | Calling the LLM requires creating a prompt and parsing its response. 31 | Here is the same diagram with a bit more detail: 32 | 33 | ```mermaid 34 | graph LR; 35 | 36 | TASK["task"]; 37 | CREATE_LLM_PROMPT["create LLM prompt"]; 38 | CALL_LLM["call LLM"]; 39 | PARSE_LLM_RESPONSE["parse LLM response"]; 40 | USE_TOOL["use tool"]; 41 | DONE["done"]; 42 | 43 | TASK-->CREATE_LLM_PROMPT 44 | CREATE_LLM_PROMPT-->CALL_LLM; 45 | CALL_LLM-->PARSE_LLM_RESPONSE; 46 | PARSE_LLM_RESPONSE-->USE_TOOL; 47 | PARSE_LLM_RESPONSE-->DONE; 48 | USE_TOOL-->CREATE_LLM_PROMPT; 49 | ``` 50 | 51 | There are other variants of agents that are much more complex and involve self-calls, planning, memory, and more. 52 | 53 | ## Agent composition 54 | 55 | Agents add several new concepts like steps, tools, and runs. You can learn more in the [agent tutorials](/tutorial/wikipedia-agent). 56 | -------------------------------------------------------------------------------- /docs/concepts/function-composition.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Function composition 6 | 7 | More complex functions in JS Agent are composed of other functions. You can pass in the component functions. This gives you full control while at the same time letting you benefit from the flow implemented in the composition. 8 | 9 | To help you compose functions more easily, many functions have `.asFunction()` or similar methods. 10 | 11 | Here is the example that creates a Twitter thread on a topic using the content of a PDF ([full example](https://github.com/lgrammel/js-agent/tree/main/examples/pdf-to-twitter-thread)): 12 | 13 | ```typescript 14 | import * as $ from "js-agent"; 15 | 16 | // ... 17 | 18 | const rewriteAsTwitterThread = $.text.splitExtractRewrite.asExtractFunction({ 19 | split: $.text.splitRecursivelyAtCharacter.asSplitFunction({ 20 | maxChunkSize: 1024 * 4, 21 | }), 22 | extract: $.text.generateText.asFunction({ 23 | model: gpt4, 24 | prompt: $.prompt.extractAndExcludeChatPrompt({ 25 | excludeKeyword: "IRRELEVANT", 26 | }), 27 | }), 28 | include: (text) => text !== "IRRELEVANT", 29 | rewrite: $.text.generateText.asFunction({ 30 | model: gpt4, 31 | prompt: async ({ text, topic }) => [ 32 | { 33 | role: "user" as const, 34 | content: `## TOPIC\n${topic}`, 35 | }, 36 | { 37 | role: "system" as const, 38 | content: `## TASK 39 | Rewrite the content below into a coherent twitter thread on the topic above. 40 | Include all relevant information about the topic. 41 | Discard all irrelevant information. 42 | Separate each tweet with ---`, 43 | }, 44 | { 45 | role: "user" as const, 46 | content: `## CONTENT\n${text}`, 47 | }, 48 | ], 49 | }), 50 | }); 51 | ``` 52 | 53 | The `rewriteAsTwitterThread` function has the following signature (and can be called directly): 54 | 55 | ```typescript 56 | type RewriteAsTwitterThreadFunction = ( 57 | options: { 58 | text: string; 59 | topic: string; 60 | }, 61 | context: $.agent.RunContext 62 | ) => PromiseLike; 63 | ``` 64 | 65 | The `context` parameter is a part of more complex functions. It records any LLM calls for cost tracking and logging. 66 | -------------------------------------------------------------------------------- /docs/concepts/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 0 3 | --- 4 | 5 | # Getting Started 6 | 7 | **JS Agent is a composable and extensible framework for creating agents with JavaScript and TypeScript.** It provides many functions for working with language models, vector stores, data loaders, and more. 8 | 9 | While creating an agent prototype is easy, increasing its reliability and robustness is complex and requires considerable experimentation. 10 | JS Agent provides robust building blocks and tooling to help you develop rock-solid agents faster. 11 | 12 | **JS Agent is currently in its initial experimental phase. Before reaching version 0.1, there may breaking changes in each release.** 13 | 14 | ## Installing JS Agent 15 | 16 | ```bash 17 | npm install js-agent 18 | ``` 19 | 20 | ## What you'll need 21 | 22 | - [Node.js](https://nodejs.org/en/download/) version 18 or above 23 | - [OpenAI API access](https://platform.openai.com/overview) 24 | - We'll add support for other model providers. 25 | -------------------------------------------------------------------------------- /docs/concepts/using-the-api.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Using the API 6 | 7 | You can use all almost all [JS Agent API](/api/modules) functions directly. This includes functions to call language models, text splitters, data loaders and more. 8 | 9 | Here is an example of splitting a text into chunks and using the OpenAI embedding API directly to get the embedding of each chunk ([full example](https://github.com/lgrammel/js-agent/tree/main/examples/split-and-embed-text)): 10 | 11 | ```typescript 12 | import * as $ from "js-agent"; 13 | 14 | const openai = $.provider.openai; 15 | 16 | const chunks = await $.text.splitRecursivelyAtToken({ 17 | text, 18 | tokenizer: openai.tokenizer.forModel({ 19 | model: "text-embedding-ada-002", 20 | }), 21 | maxChunkSize: 128, 22 | }); 23 | 24 | const embeddings = []; 25 | for (const chunk of chunks) { 26 | const response = await openai.api.generateEmbedding({ 27 | model: "text-embedding-ada-002", 28 | apiKey: openAiApiKey, 29 | input: chunk, 30 | }); 31 | 32 | embeddings.push({ 33 | chunk, 34 | embedding: response.data[0].embedding, 35 | }); 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/docs/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /docs/old/actions.md: -------------------------------------------------------------------------------- 1 | # How do actions work? 2 | 3 | ## Concepts 4 | 5 | ### [Action](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/action/Action.ts) 6 | 7 | Actions are descriptions of operations that the LLM can decide to do. The LLM is informed about the available actions in the prompt, and if they are part of the response, they are parsed. 8 | 9 | ### [Step](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/step/Step.ts) 10 | 11 | The main operation of one iteration of the agent. 12 | 13 | ### Tool 14 | 15 | Tools run code on behalf of the agent. The LLM can decide to use tools by choosing a [ToolAction](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/action/tool/ToolAction.ts) in its response. ToolActions create [ToolSteps](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/action/tool/ToolStep.ts), which run the [ToolExecutor](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/action/tool/ToolExecutor.ts). 16 | 17 | ## Flow 18 | 19 | 1. Actions are registered in the [ActionRegistry](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/action/ActionRegistry.ts). 20 | 2. Descriptions of the actions are included in the OpenAI prompt by calling actionRegistry.getAvailableActions(), e.g. through the [AvailableActionsSectionPrompt](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/prompt/AvailableActionsSectionPrompt.ts). 21 | 3. actionRegistry.getAvailableActionInstructions() generates explanation and a detailed list of all actions using their examples and the formatter being used. For the [JsonActionFormat](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/action/format/JsonActionFormat.ts), that prompt section looks e.g. like tihs: 22 | 23 | ``` 24 | ## AVAILABLE ACTIONS 25 | You can perform the following actions using JSON: 26 | 27 | ### tool.search-wikipedia 28 | Search wikipedia using a search term. Returns a list of pages. 29 | Syntax: 30 | { 31 | "action": "tool.search-wikipedia", 32 | "query": "{search query}" 33 | } 34 | 35 | ### tool.read-wikipedia-article 36 | Read a wikipedia article and summarize it considering the query. 37 | Syntax: 38 | { 39 | "action": "tool.read-wikipedia-article", 40 | "url": "https://en.wikipedia.org/wiki/Artificial_intelligence", 41 | "topic": "{query that you are answering}" 42 | } 43 | 44 | ### done 45 | Indicate that you are done with the task. 46 | Syntax: 47 | { 48 | "action": "done" 49 | } 50 | 51 | ## RESPONSE FORMAT (ALWAYS USE THIS FORMAT) 52 | 53 | Explain and describe your reasoning step by step. 54 | Then use the following format to specify the action you want to perform next: 55 | 56 | { 57 | "action": "an action", 58 | "param1": "a parameter value", 59 | "param2": "another parameter value" 60 | } 61 | 62 | You must always use exactly one action with the correct syntax per response. 63 | Each response must precisely follow the action syntax. 64 | ``` 65 | 66 | 4. The LLM response can include an action after the text. It is parsed using the [ActionFormat](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/action/format/ActionFormat.ts) parse method, e.g. in [DynamicCompositeStep.generateNextStep()](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/step/DynamicCompositeStep.ts) 67 | 5. The action is then retrieved from the registry and an action step is created (also in DynamicCompositeStep). 68 | 6. When the step is executed and it is a [ToolStep](https://github.com/lgrammel/js-agent/blob/main/packages/agent/src/action/tool/ToolStep.ts) (which is created by ToolActions), then its executor is invoked. 69 | 7. The tool executor runs the actual code. 70 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "cd .. && pnpm install && cd docs && docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.4.0", 19 | "@docusaurus/preset-classic": "2.4.0", 20 | "@docusaurus/theme-mermaid": "^2.4.0", 21 | "@mdx-js/react": "^1.6.22", 22 | "clsx": "^1.2.1", 23 | "prism-react-renderer": "^1.3.5", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "2.4.0", 29 | "@tsconfig/docusaurus": "^1.0.5", 30 | "docusaurus-plugin-typedoc": "^0.19.2", 31 | "typedoc": "^0.24.6", 32 | "typedoc-plugin-markdown": "^3.15.2", 33 | "typedoc-plugin-zod": "^1.0.2", 34 | "typescript": "^4.7.4" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.5%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "engines": { 49 | "node": ">=16.14" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 4 | const sidebars = { 5 | api: [ 6 | { 7 | type: "autogenerated", 8 | dirName: ".", 9 | }, 10 | ], 11 | }; 12 | 13 | module.exports = sidebars; 14 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import styles from "./styles.module.css"; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | // Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: "Create agents quickly", 14 | description: ( 15 | <> 16 | JS Agent provides many concepts, pre-defined prompts, and tools to help 17 | you create agents quickly. 18 | 19 | ), 20 | }, 21 | { 22 | title: "Run agents in a server", 23 | description: ( 24 | <> 25 | JS Agent contains a HTTP server that host multiple agents. Agent runs 26 | can be started, stopped, and observed via HTTP API. 27 | 28 | ), 29 | }, 30 | { 31 | title: "Load data", 32 | description: <>Loaders for reading PDFs and websites, 33 | }, 34 | { 35 | title: "Calculate costs", 36 | description: ( 37 | <> 38 | The cost package contains functions 39 | that help you calculate the cost of API calls and agent runs. 40 | 41 | ), 42 | }, 43 | ]; 44 | 45 | function Feature({ title, description }: FeatureItem) { 46 | return ( 47 |
48 | {/*
49 | 50 |
*/} 51 |
52 |

{title}

53 |

{description}

54 |
55 |
56 | ); 57 | } 58 | 59 | export default function HomepageFeatures(): JSX.Element { 60 | return ( 61 |
62 |
63 |
64 | {FeatureList.map((props, idx) => ( 65 | 66 | ))} 67 |
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import Link from "@docusaurus/Link"; 4 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 5 | import Layout from "@theme/Layout"; 6 | import HomepageFeatures from "@site/src/components/HomepageFeatures"; 7 | 8 | import styles from "./index.module.css"; 9 | 10 | function HomepageHeader() { 11 | const { siteConfig } = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 |

{siteConfig.title}

16 |

{siteConfig.tagline}

17 |
18 | 19 | Learn more 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | /* - 5min ⏱️ */ 28 | 29 | export default function Home(): JSX.Element { 30 | const { siteConfig } = useDocusaurusContext(); 31 | return ( 32 | 36 | 37 |
38 | 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/js-agent/bc85c63380a61ff8e773332832f93b27271d3328/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/js-agent/bc85c63380a61ff8e773332832f93b27271d3328/docs/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/js-agent/bc85c63380a61ff8e773332832f93b27271d3328/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/js-agent/bc85c63380a61ff8e773332832f93b27271d3328/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/tutorial/wikipedia-agent/add-agent.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | title: Add agent 4 | --- 5 | 6 | # Add agent 7 | 8 | Now we're ready to create a basic agent. Let's update the `runWikipediaAgent` function with the following code: 9 | 10 | ```typescript 11 | const chatGpt = $.provider.openai.chatModel({ 12 | apiKey: openAiApiKey, 13 | model: "gpt-3.5-turbo", 14 | }); 15 | 16 | return $.runAgent<{ task: string }>({ 17 | properties: { task }, 18 | agent: $.step.generateNextStepLoop({ 19 | actions: [], 20 | actionFormat: $.action.format.flexibleJson(), 21 | prompt: async ({ runState: { task } }) => [ 22 | { role: "user" as const, content: `${task}` }, 23 | ], 24 | model: chatGpt, 25 | }), 26 | controller: $.agent.controller.maxSteps(3), 27 | observer: $.agent.observer.showRunInConsole({ 28 | name: "Wikipedia Agent", 29 | }), 30 | }); 31 | ``` 32 | 33 | When you run it, it'll output the basic LLM answer to the console until the maximum number of steps is reached: 34 | 35 | ```bash 36 | ❯ npx ts-node src/agent.ts "how many people live in BC, Canada?" 37 | ### Wikipedia Agent ### 38 | { task: 'how many people live in BC, Canada?' } 39 | 40 | Thinking… 41 | As an AI language model, I do not have access to real-time data. However, according to the latest census conducted in 2016, the population of British Columbia, Canada was approximately 4.6 million. 42 | 43 | Thinking… 44 | As an AI language model, I do not have access to real-time data. However, according to the latest census conducted in 2016, the population of British Columbia, Canada was approximately 4.6 million. 45 | 46 | Thinking… 47 | As an AI language model, I do not have access to real-time data. However, according to the latest census conducted in 2016, the population of British Columbia, Canada was approximately 4.6 million. 48 | 49 | Cancelled: Maximum number of steps (3) exceeded. 50 | ``` 51 | 52 | Let's dig into the code. 53 | 54 | `$.runAgent` runs an agent. 55 | It is typed to the properties of the agent, which are also its input. 56 | We pass in the `task` as a property: 57 | 58 | ```typescript 59 | return $.runAgent<{ task: string }>({ 60 | properties: { task }, 61 | // ... 62 | }); 63 | ``` 64 | 65 | The `agent` property contains the root step of the agent. 66 | We use a `$.step.generateNextStepLoop` step, which generates steps using the LLM until the agent is done: 67 | 68 | ```typescript 69 | return $.runAgent<...>({ 70 | // ... 71 | agent: $.step.generateNextStepLoop({ 72 | actions: [], 73 | actionFormat: $.action.format.flexibleJson(), 74 | prompt: async ({ runState: { task } }) => [ 75 | { role: "user" as const, content: `${task}` }, 76 | ], 77 | model: chatGpt, 78 | }), 79 | ``` 80 | 81 | The loop is configured with our earlier prompt function and the `chatGpt` model. 82 | This prompt is used when calling the `chatGpt` model generate function. 83 | We'll configure and talk about the actions later. 84 | 85 | Because the agent has no actions yet and does not know when to stop, we limit the maximum number of steps to 3: 86 | 87 | ```typescript 88 | return $.runAgent<...>({ 89 | // ... 90 | controller: $.agent.controller.maxSteps(3), 91 | ``` 92 | 93 | And finally, we use an observer that outputs the agent's run to the console: 94 | 95 | ```typescript 96 | return $.runAgent<...>({ 97 | // ... 98 | observer: $.agent.observer.showRunInConsole({ 99 | name: "Wikipedia Agent", 100 | }), 101 | ``` 102 | 103 | With the basic agent in place, let's add some tools next. 104 | -------------------------------------------------------------------------------- /docs/tutorial/wikipedia-agent/create-agent-ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | title: Create agent.ts 4 | --- 5 | 6 | # Create agent.ts 7 | 8 | `src/agent.ts` will contain the Wikipedia agent. To get started, add the following content: 9 | 10 | ```typescript 11 | const task = process.argv.slice(2).join(" "); 12 | 13 | runWikipediaAgent() 14 | .then(() => {}) 15 | .catch((error) => { 16 | console.error(error); 17 | }); 18 | 19 | async function runWikipediaAgent() { 20 | console.log(task); 21 | } 22 | ``` 23 | 24 | You can now run it with e.g.: 25 | 26 | ```bash 27 | ❯ npx ts-node src/agent.ts "how many people live in BC, Canada?" 28 | ``` 29 | 30 | At this point, it only outputs the task to the console. 31 | -------------------------------------------------------------------------------- /docs/tutorial/wikipedia-agent/create-read-article-tool.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | title: Create read article tool 4 | --- 5 | 6 | # Create read article tool 7 | 8 | The read article action is implemented using the JS Agent `extractInformationFromWebpage` tool: 9 | 10 | ```typescript 11 | const readWikipediaArticleAction = $.tool.extractInformationFromWebpage({ 12 | id: "read-wikipedia-article", 13 | description: 14 | "Read a wikipedia article and summarize it considering the query.", 15 | inputExample: { 16 | url: "https://en.wikipedia.org/wiki/Artificial_intelligence", 17 | topic: "{query that you are answering}", 18 | }, 19 | execute: $.tool.executeExtractInformationFromWebpage({ 20 | extract: $.text.extractRecursively.asExtractFunction({ 21 | split: $.text.splitRecursivelyAtToken.asSplitFunction({ 22 | tokenizer: $.provider.openai.tokenizer.forModel({ 23 | model: "gpt-3.5-turbo", 24 | }), 25 | maxChunkSize: 2048, // needs to fit into a gpt-3.5-turbo prompt 26 | }), 27 | extract: $.text.generateText.asFunction({ 28 | prompt: $.prompt.extractChatPrompt(), 29 | model: chatGpt, 30 | }), 31 | }), 32 | }), 33 | }); 34 | ``` 35 | 36 | In addition to the `id` and the `description`, the action has an `inputExample` that will be shown to the LLM. 37 | Input examples help with guiding the LLM to take the right action. 38 | Every tool has a default input example that can be overridden. 39 | 40 | The page is then summarized using text extraction. 41 | It is split recursively until the chunks are small enough for `gpt-3.5-turbo` to handle. 42 | `gpt-3.5-turbo` is used to generate a summary for each chunk and the concatenated summaries. 43 | 44 | `$.prompt.extractChatPrompt()`, which is part of JS Agent, contains the following prompt: 45 | 46 | ```typescript 47 | async ({ text, topic }: { text: string; topic: string }) => [ 48 | { 49 | role: "user" as const, 50 | content: `## TOPIC\n${topic}`, 51 | }, 52 | { 53 | role: "system" as const, 54 | content: `## ROLE 55 | You are an expert at extracting information. 56 | You need to extract and keep all the information on the topic above topic from the text below. 57 | Only include information that is directly relevant for the topic.`, 58 | }, 59 | { 60 | role: "user" as const, 61 | content: `## TEXT\n${text}`, 62 | }, 63 | ]; 64 | ``` 65 | 66 | Now that we have created a summarization tool, we can put everything together and craft a better agent prompt. 67 | -------------------------------------------------------------------------------- /docs/tutorial/wikipedia-agent/create-search-tool.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | title: Create search tool 4 | --- 5 | 6 | # Create Wikipedia search tool 7 | 8 | ## Create a programmable search engine 9 | 10 | First, you need to create a [programmable search engine](https://programmablesearchengine.google.com/about/) for Wikipedia. 11 | 12 | When you set up the search engine, configure the site to be `en.wikipedia.org/*`. 13 | The search engine id (the `cx` parameter) is on the overview page. 14 | You can get the [search engine key in the documentation](https://developers.google.com/custom-search/v1/introduction) ("Get a Key"; requires a Google project). 15 | 16 | ## Create the search action 17 | 18 | JS Agent has a built-in tool for using programmable search engines. 19 | You can use it to create a search action. 20 | 21 | :::info 22 | Tools are actions that run (potentially external) code in some fashion. They don't affect the control flow directly. There are other kinds of actions, e.g., the "done" action, that an agent can select. 23 | ::: 24 | 25 | ```typescript 26 | const searchWikipediaAction = $.tool.programmableGoogleSearchEngineAction({ 27 | id: "search-wikipedia", 28 | description: "Search wikipedia using a search term. Returns a list of pages.", 29 | execute: $.tool.executeProgrammableGoogleSearchEngineAction({ 30 | key: "your search engine key", 31 | cx: "your search engine id", 32 | }), 33 | }); 34 | ``` 35 | 36 | The `id` and `description` parameters are included in the LLM prompt. 37 | It is important to choose names and descriptions that are easy to understand for the LLM, because they will determine if and when the agent decides to use this action. 38 | 39 | The `execution` parameter contains the function that is running the tool code. 40 | It provides additional flexibility, e.g., using executors that run in a Docker container. 41 | -------------------------------------------------------------------------------- /docs/tutorial/wikipedia-agent/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | title: Tutorial - Wikipedia Agent 4 | --- 5 | 6 | # Overview 7 | 8 | In this tutorial, you'll learn how to create an agent that answers questions by searching and reading [Wikipedia](https://www.wikipedia.org/) articles. 9 | You can find the complete code in the [Wikipedia agent example](https://github.com/lgrammel/js-agent/tree/main/examples/wikipedia). 10 | 11 | The agent will use the following components: 12 | 13 | - OpenAI `gpt-3.5-turbo` chat completion model 14 | - A loop in which the agent determines and executes steps ("GenerateNextStepLoop") 15 | - A custom prompt 16 | - Wikipedia search tool (implemented using a [Programmable Search Engine](https://programmablesearchengine.google.com/)) 17 | - Wikipedia article reading tool 18 | - Command line interface and console logger that shows the agent's progress 19 | 20 | This is the high-level flow of the agent: 21 | 22 | ```mermaid 23 | graph TD; 24 | CONSOLE_LOGGER["log to console"]; 25 | DONE["done"] 26 | 27 | CLI-->TASK; 28 | agent-->CONSOLE_LOGGER; 29 | 30 | subgraph agent["Wikipedia QA Agent"] 31 | TASK["task"]; 32 | CREATE_PROMPT["create Prompt"] 33 | CALL_LLM["call gpt-3.5-turbo"] 34 | PARSE_GPT_RESPONSE["parse gpt-3.5-turbo response"] 35 | SEARCH_WIKIPEDIA["search Wikipedia"] 36 | READ_WIKIPEDIA["read Wikipedia article"] 37 | 38 | TASK-->CREATE_PROMPT 39 | CREATE_PROMPT-->CALL_LLM; 40 | CALL_LLM-->PARSE_GPT_RESPONSE; 41 | PARSE_GPT_RESPONSE-->SEARCH_WIKIPEDIA; 42 | SEARCH_WIKIPEDIA-->CREATE_PROMPT; 43 | PARSE_GPT_RESPONSE-->READ_WIKIPEDIA; 44 | READ_WIKIPEDIA-->CREATE_PROMPT; 45 | PARSE_GPT_RESPONSE-->CALL_LLM; 46 | end 47 | 48 | PARSE_GPT_RESPONSE-->DONE; 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/tutorial/wikipedia-agent/setup-actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | title: Setup actions 4 | --- 5 | 6 | # Setup actions 7 | 8 | ## Add actions to the agent 9 | 10 | Now that we have a search Wikipedia and a read article action, we can set up the actions in the Wikipedia agent. 11 | Let's add them to the actions section of `$.step.generateNextStepLoop`: 12 | 13 | ```typescript 14 | return $.runAgent<...>({ 15 | // ... 16 | agent: $.step.generateNextStepLoop({ 17 | actions: [searchWikipediaAction, readWikipediaArticleAction], 18 | actionFormat: $.action.format.flexibleJson(), 19 | ``` 20 | 21 | The `actionFormat` parses the first flat JSON object in the LLM output. 22 | It is specifically designed for ChatGPT, which tends to output the JSON object in various places in the response. 23 | 24 | ## Create a better prompt 25 | 26 | The agent has yet to be made aware of the actions and it does not know that it should read Wikipedia articles. 27 | Let's improve the agent prompt. 28 | 29 | ```typescript 30 | return $.runAgent<...>({ 31 | // ... 32 | prompt: $.prompt.concatChatPrompts( 33 | async ({ runState: { task } }) => [ 34 | { 35 | role: "system", 36 | content: `## ROLE 37 | You are an knowledge worker that answers questions using Wikipedia content. 38 | 39 | ## CONSTRAINTS 40 | All facts for your answer must be from Wikipedia articles that you have read. 41 | 42 | ## TASK 43 | ${task}`, 44 | }, 45 | ], 46 | $.prompt.availableActionsChatPrompt(), 47 | $.prompt.recentStepsChatPrompt({ maxSteps: 6 }) 48 | ), 49 | ``` 50 | 51 | Let's dissect the prompt. 52 | We first tell the model about its general role, and then we instruct it always to read Wikipedia articles to find the answer and give it the task. 53 | 54 | ``` 55 | ## ROLE 56 | You are an knowledge worker that answers questions using Wikipedia content. 57 | 58 | ## CONSTRAINTS 59 | All facts for your answer must be from Wikipedia articles that you have read. 60 | 61 | ## TASK 62 | ${task} 63 | ``` 64 | 65 | The next part informs the model about the available actions. 66 | After that, we ensure to include the last steps the model took in the prompt for the next iteration. 67 | This provides some basic memory required for moving the agent forward in its task. 68 | 69 | ```typescript 70 | return $.runAgent<...>({ 71 | // ... 72 | prompt: $.prompt.concatChatPrompts( 73 | // ... 74 | $.prompt.availableActionsChatPrompt(), 75 | $.prompt.recentStepsChatPrompt({ maxSteps: 6 }) 76 | ), 77 | ``` 78 | 79 | The different prompts are concatenated using `$.prompt.concatChatPrompts`. 80 | 81 | ## Update the maximum steps 82 | 83 | The agent is now ready to be run. 84 | We increase the maximum number of steps to 10 to provide the agent with more time to find the answer. 85 | 86 | ```typescript 87 | return $.runAgent<...>({ 88 | // ... 89 | controller: $.agent.controller.maxSteps(10), 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/tutorial/wikipedia-agent/setup-llm-model.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | title: Setup LLM model 4 | --- 5 | 6 | # Setup the LLM model 7 | 8 | Next, we'll set up the LLM model and create the agent loop with a basic prompt. 9 | 10 | ## Load OpenAI API key 11 | 12 | Update the code to load the OpenAI API key from the environment and inject it into the agent: 13 | 14 | ```typescript 15 | const task = process.argv.slice(2).join(" "); 16 | 17 | const openAiApiKey = process.env.OPENAI_API_KEY; 18 | if (!openAiApiKey) { 19 | throw new Error("OPENAI_API_KEY is not set"); 20 | } 21 | 22 | runWikipediaAgent() 23 | .then(() => {}) 24 | .catch((error) => { 25 | console.error(error); 26 | }); 27 | 28 | async function runWikipediaAgent() { 29 | console.log(openAiApiKey); 30 | console.log(task); 31 | } 32 | ``` 33 | 34 | ## Create the LLM model 35 | 36 | First, import JS Agent: 37 | 38 | ```typescript 39 | import * as $ from "js-agent"; 40 | ``` 41 | 42 | You can then create the chat model in `runWikipediaAgent`: 43 | 44 | ```typescript 45 | const chatGpt = $.provider.openai.chatModel({ 46 | apiKey: openAiApiKey, 47 | model: "gpt-3.5-turbo", 48 | }); 49 | ``` 50 | 51 | ## Call the model with a basic prompt 52 | 53 | Once you have a model, you can call it directly with a basic prompt: 54 | 55 | ```typescript 56 | const fullResponse = await chatGpt.generate([ 57 | { role: "user" as const, content: task }, 58 | ]); 59 | ``` 60 | 61 | And extract the main output from its response: 62 | 63 | ```typescript 64 | const output = await chatGpt.extractOutput(fullResponse); 65 | 66 | console.log(output); 67 | ``` 68 | 69 | Putting this together, this is the current code: 70 | 71 | ```typescript 72 | import * as $ from "js-agent"; 73 | 74 | const task = process.argv.slice(2).join(" "); 75 | 76 | const openAiApiKey = process.env.OPENAI_API_KEY; 77 | if (!openAiApiKey) { 78 | throw new Error("OPENAI_API_KEY is not set"); 79 | } 80 | 81 | runWikipediaAgent() 82 | .then(() => {}) 83 | .catch((error) => { 84 | console.error(error); 85 | }); 86 | 87 | async function runWikipediaAgent() { 88 | const chatGpt = $.provider.openai.chatModel({ 89 | apiKey: openAiApiKey, 90 | model: "gpt-3.5-turbo", 91 | }); 92 | 93 | const fullResponse = await chatGpt.generate([ 94 | { role: "user" as const, content: task }, 95 | ]); 96 | 97 | const output = await chatGpt.extractOutput(fullResponse); 98 | 99 | console.log(task); 100 | console.log(output); 101 | } 102 | ``` 103 | 104 | When you run it, it'll use the knowledge that's trained into the LLM to answer the question: 105 | 106 | ```bash 107 | ❯ npx ts-node src/agent.ts "how many people live in BC, Canada?" 108 | how many people live in BC, Canada? 109 | As an AI language model, I do not have access to real-time data. However, according to the latest census conducted in 2016, the population of British Columbia, Canada was approximately 4.6 million. 110 | ``` 111 | -------------------------------------------------------------------------------- /docs/tutorial/wikipedia-agent/setup-project.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | title: Setup project 4 | --- 5 | 6 | # Setup Project 7 | 8 | ## Pre-requisites 9 | 10 | This tutorial assumes that you have Node.js (v18 or newer) installed. 11 | You also need access to the OpenAI API. 12 | 13 | ## Create a new Node.js project 14 | 15 | ```bash 16 | mkdir wikipedia-agent 17 | cd wikipedia-agent 18 | npm init -y 19 | mkdir src 20 | ``` 21 | 22 | ## Setup TypeScript 23 | 24 | ```bash 25 | npm install --save-dev typescript ts-node @types/node 26 | npx tsc --init --rootDir src --outDir .build 27 | ``` 28 | 29 | ## Install JS Agent 30 | 31 | ```bash 32 | npm install js-agent 33 | ``` 34 | 35 | At this point, you should have a basic Node.js project that has TypeScript and JS Agent installed. 36 | -------------------------------------------------------------------------------- /examples/babyagi/README.md: -------------------------------------------------------------------------------- 1 | # JS Agent BabyAGI (Server) 2 | 3 | JS Agent server implementation of [BabyAGI](https://github.com/yoheinakajima/babyagi) by [@yoheinakajima](https://twitter.com/yoheinakajima). 4 | 5 | It is implemented as a single planner step and does not use memory or actions. The main loop that executes the top task from a task list and then updates the task list was extracted into "UpdateTasksLoop". 6 | 7 | ## JS Agent features used 8 | 9 | - Agent server with HTTP API 10 | - expose run as log 11 | - calculate cost 12 | - OpenAI text completion model (`text-davinci-003`) 13 | - `UpdateTasksLoop` planning loop 14 | - Creating typed LLM functions with prompt and output processor using `$.text.generate` 15 | 16 | ## Usage 17 | 18 | 1. Create .env file with the following content: 19 | 20 | ``` 21 | OPENAI_API_KEY="YOUR_OPENAI_API_KEY" 22 | ``` 23 | 24 | 2. Build the project: 25 | 26 | ```sh 27 | # in root folder: 28 | pnpm install 29 | pnpm nx run-many --target=build 30 | 31 | # in examples/babyagi folder: 32 | pnpm build 33 | ``` 34 | 35 | 3. Start the server. It runs on port `30800` by default. You can set the `--host` and `--port` params to change the host and port the server runs on. 36 | 37 | ```sh 38 | pnpm start 39 | ``` 40 | 41 | 4. Create an agent run using a `POST` request to `/agent/babyagi`. The response contains the `runId`: 42 | 43 | ```bash 44 | curl -X POST -H "Content-Type: application/json" -d '{"objective":"solve world hunger"}' http://127.0.0.1:30800/agent/babyagi 45 | {"runId":"bsFcdSuvQQONvG5zxf1G_g-0"}% 46 | ``` 47 | 48 | 5. Use the run id to access the current run state in the browser. 49 | 50 | `http://localhost:30800/agent/babyagi/run/bsFcdSuvQQONvG5zxf1G_g-0` 51 | 52 | 6. Start the run (with the run id): 53 | 54 | ```bash 55 | ❯ curl -X POST http://127.0.0.1:30800/agent/babyagi/run/bsFcdSuvQQONvG5zxf1G_g-0/start 56 | {"runId":"bsFcdSuvQQONvG5zxf1G_g-0"}% 57 | ``` 58 | 59 | 7. You can cancel the run with a call to the cancel route (with the run id): 60 | 61 | ```bash 62 | ❯ curl -X POST -H "Content-Type: application/json" -d '{"reason": "need to shut down computer"}' http://127.0.0.1:30800/agent/babyagi/run/bxynsv4USkGoawtCYhte-w-0/cancel 63 | ``` 64 | -------------------------------------------------------------------------------- /examples/babyagi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-agent/example-babyagi", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "pnpm js-agent-server -f .build/agent", 7 | "build": "tsc" 8 | }, 9 | "private": true, 10 | "dependencies": { 11 | "js-agent": "*", 12 | "zod": "3.21.4" 13 | }, 14 | "devDependencies": { 15 | "typescript": "5.0.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/babyagi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "sourceMap": true, 6 | "target": "es2020", 7 | "lib": ["es2020", "dom"], 8 | "module": "commonjs", 9 | "types": ["node"], 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "rootDir": "./agent", 14 | "outDir": "./.build/agent" 15 | }, 16 | "include": ["agent/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /examples/javascript-developer/.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | node_modules 3 | drive/** -------------------------------------------------------------------------------- /examples/javascript-developer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | # Install dumb-init. Used to enable response to SIGTERM and SIGINT. 4 | RUN apk update && apk add dumb-init && apk add git && apk add ca-certificates 5 | 6 | RUN npm install -g npm@9.6.2 7 | 8 | # PROJECT SPECIFIC INSTALLS 9 | # Specific to js-agent example. Adjust to fit your own project: 10 | # RUN npm install -g pnpm 11 | # RUN apk add python3 && apk add build-base 12 | 13 | # Security: Run as non-root user. 14 | USER node 15 | 16 | WORKDIR /home/service 17 | 18 | COPY --chown=node:node .build/gptagent-executor.js /home/service 19 | COPY --chown=node:node ./node_modules/@dqbd /home/service/node_modules/@dqbd 20 | 21 | WORKDIR /home/service/repository 22 | 23 | # Use dumb-init to enable response to SIGTERM and SIGINT. 24 | CMD ["dumb-init", "node", "../gptagent-executor.js"] -------------------------------------------------------------------------------- /examples/javascript-developer/README.md: -------------------------------------------------------------------------------- 1 | # JS Agent JavaScript Developer 2 | 3 | An automated developer agent that works in a docker container. It can read files, write files and execute commands. You can adjust it for your project and use it to document code, write tests, update tests and features, etc. 4 | 5 | ## JS Agent features used 6 | 7 | - OpenAI chat completion model (`gpt-4`) 8 | - Tool execution separation with executor running in Docker container (to prevent command line actions and file edits from affecting the host machine) 9 | - Agent starts with setup steps (`FixedStepsLoop`) 10 | - Multiple agent run properties 11 | - `GenerateNextStepLoop` loop with tools (read file, write file, run, command, ask user) and custom prompt 12 | - Cost calculation and extracting information from LLM calls after the run 13 | 14 | ## Usage 15 | 16 | ```sh 17 | export OPENAI_API_KEY=sk-... 18 | 19 | # in root folder: 20 | pnpm install 21 | pnpm nx run-many --target=build 22 | ``` 23 | 24 | Make sure to `cd examples/javascript-developer` before running the following commands: 25 | 26 | ```sh 27 | mkdir drive 28 | 29 | pnpm build 30 | pnpm run-executor 31 | 32 | pnpm run-agent `cat examples/helloworld/task.txt` # or any other instruction 33 | ``` 34 | 35 | The `drive` folder contains the shared files between the host and the docker container. 36 | 37 | ## How to use the JS Agent developer in your own project 38 | 39 | 1. Clone the git repository that the agent should work on into the drive folder, e.g., 40 | `git clone https://github.com/lgrammel/js-agent.git drive` 41 | 42 | 2. Configure the Dockerfile to install any libraries that you need for your project. 43 | There are existing examples for JS-Agent in the the Dockerfile. 44 | 45 | 3. Update ` src/main.ts` with project-specific instructions and setup command. 46 | There are existing examples for JS-Agent in the the `src/main.ts` file. 47 | 48 | 4. Build & run the docker container 49 | 50 | 5. Run the agent with a task. 51 | Put any needed guidance and finish criteria into the task instructions. 52 | Reference files by their relative path from the workspace root (e.g., "packages/agent/src/index.ts"). 53 | 54 | ## Example tasks 55 | 56 | ``` 57 | Write a unit test for packages/agent/src/action/format/JsonActionFormat. Cover the main path and edge cases to get good coverage. 58 | ``` 59 | 60 | ## Example Output 61 | 62 | ![wikipedia](https://github.com/lgrammel/js-agent/blob/main/examples/javascript-developer/screenshot/autodev-001.png) 63 | -------------------------------------------------------------------------------- /examples/javascript-developer/bin/build-executor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # needs to be run from parent directory 4 | 5 | rm -f .build/gptagent-executor.js 6 | mkdir -p .build 7 | npx esbuild executor.mjs --bundle --platform=node --loader:.node=file --outfile=.build/gptagent-executor.js 8 | 9 | rm -rf ./node_modules/@dqbd 10 | cp -a ../../node_modules/.pnpm/@dqbd+tiktoken@1.0.7/node_modules/@dqbd ./node_modules 11 | 12 | PLATFORM=linux/amd64 13 | if [[ $(uname -m) == "aarch64" ]]; then 14 | PLATFORM=linux/arm64 15 | fi 16 | docker build --platform "$PLATFORM" -t gptagent-javascript-developer . 17 | -------------------------------------------------------------------------------- /examples/javascript-developer/bin/run-executor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Run js-agent executor container" 4 | echo "Drive: `pwd`/drive" 5 | echo "" 6 | 7 | PLATFORM=linux/amd64 8 | if [[ $(uname -m) == "aarch64" ]]; then 9 | PLATFORM=linux/arm64 10 | fi 11 | 12 | docker run -i -t -p 3001:3001 \ 13 | --env PORT=3001 \ 14 | --env HOST=0.0.0.0 \ 15 | --env WORKSPACE=/home/service/repository \ 16 | --volume "`pwd`/drive:/home/service/repository" \ 17 | --platform "$PLATFORM" \ 18 | gptagent-javascript-developer:latest 19 | -------------------------------------------------------------------------------- /examples/javascript-developer/example/cipher/task.txt: -------------------------------------------------------------------------------- 1 | Create an implementation of the rotational cipher, also sometimes called the Caesar cipher. 2 | 3 | The Caesar cipher is a simple shift cipher that relies on transposing all the letters in the alphabet using an integer key between 0 and 26. Using a key of 0 or 26 will always yield the same output due to modular arithmetic. The letter is shifted for as many values as the value of the key. 4 | 5 | The general notation for rotational ciphers is ROT + . The most commonly used rotational cipher is ROT13. 6 | 7 | A ROT13 on the Latin alphabet would be as follows: 8 | 9 | Plain: abcdefghijklmnopqrstuvwxyz 10 | Cipher: nopqrstuvwxyzabcdefghijklm 11 | It is stronger than the Atbash cipher because it has 27 possible keys, and 25 usable keys. 12 | 13 | Ciphertext is written out in the same formatting as the input including spaces and punctuation. 14 | 15 | Examples 16 | ROT5 omg gives trl 17 | ROT0 c gives c 18 | ROT26 Cool gives Cool 19 | ROT13 The quick brown fox jumps over the lazy dog. gives Gur dhvpx oebja sbk whzcf bire gur ynml qbt. 20 | ROT13 Gur dhvpx oebja sbk whzcf bire gur ynml qbt. gives The quick brown fox jumps over the lazy dog. -------------------------------------------------------------------------------- /examples/javascript-developer/example/helloworld/task-modify.txt: -------------------------------------------------------------------------------- 1 | Change the existing "Hello, World" program such that a name can be entered through a command line parameter. 2 | 3 | The output should include the name, e.g. "Hello, Peter!". -------------------------------------------------------------------------------- /examples/javascript-developer/example/helloworld/task-typescript.txt: -------------------------------------------------------------------------------- 1 | Change the existing HelloWorld program to TypeScript and make sure it has the correct output. -------------------------------------------------------------------------------- /examples/javascript-developer/example/helloworld/task.txt: -------------------------------------------------------------------------------- 1 | The classical introductory exercise. Just say "Hello, World!". 2 | 3 | "Hello, World!" is the traditional first program for beginning programming in a new language or environment. 4 | 5 | The objectives are simple: 6 | 7 | Write a program that prints the string "Hello, World!". 8 | 9 | Write the program in JavaScript. -------------------------------------------------------------------------------- /examples/javascript-developer/example/roman-numbers/task-add-tests.txt: -------------------------------------------------------------------------------- 1 | The repository contains a function to convert numbers to Roman numerals. 2 | 3 | Write a comprehensive test suite using Mocha and run it to verify the code behaves as expected. -------------------------------------------------------------------------------- /examples/javascript-developer/example/roman-numbers/task.txt: -------------------------------------------------------------------------------- 1 | Write a function that converts from normal numbers to Roman Numerals. 2 | 3 | The Romans were a clever bunch. They conquered most of Europe and ruled it for hundreds of years. They invented concrete and straight roads and even bikinis. One thing they never discovered though was the number zero. This made writing and dating extensive histories of their exploits slightly more challenging, but the system of numbers they came up with is still in use today. For example the BBC uses Roman numerals to date their programmes. 4 | 5 | The Romans wrote numbers using letters - I, V, X, L, C, D, M. (notice these letters have lots of straight lines and are hence easy to hack into stone tablets). 6 | 7 | 1 => I 8 | 10 => X 9 | 7 => VII 10 | There is no need to be able to convert numbers larger than about 3000. (The Romans themselves didn't tend to go any higher) 11 | 12 | Wikipedia says: Modern Roman numerals ... are written by expressing each digit separately starting with the left most digit and skipping any digit with a value of zero. 13 | 14 | To see this in practice, consider the example of 1990. 15 | 16 | In Roman numerals 1990 is MCMXC: 17 | 18 | 1000=M 900=CM 90=XC 19 | 20 | 2008 is written as MMVIII: 21 | 22 | 2000=MM 8=VIII -------------------------------------------------------------------------------- /examples/javascript-developer/executor.mjs: -------------------------------------------------------------------------------- 1 | import $ from "js-agent"; 2 | 3 | $.tool.executor.runToolExecutor({ 4 | tools: [ 5 | $.tool.readFile({ 6 | execute: $.tool.executeReadFile({ 7 | workspacePath: process.env.WORKSPACE, 8 | }), 9 | }), 10 | $.tool.writeFile({ 11 | execute: $.tool.executeWriteFile({ 12 | workspacePath: process.env.WORKSPACE, 13 | }), 14 | }), 15 | $.tool.runCommand({ 16 | execute: $.tool.executeRunCommand({ 17 | workspacePath: process.env.WORKSPACE, 18 | }), 19 | }), 20 | ], 21 | }); 22 | -------------------------------------------------------------------------------- /examples/javascript-developer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-agent/example-javascript-developer", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "bin/build-executor.sh", 7 | "run-executor": "bin/run-executor.sh", 8 | "run-agent": "ts-node ./src/main.ts" 9 | }, 10 | "private": true, 11 | "dependencies": { 12 | "dotenv": "16.0.3", 13 | "js-agent": "*" 14 | }, 15 | "devDependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /examples/javascript-developer/screenshot/autodev-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/js-agent/bc85c63380a61ff8e773332832f93b27271d3328/examples/javascript-developer/screenshot/autodev-001.png -------------------------------------------------------------------------------- /examples/javascript-developer/src/main.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { runDeveloperAgent } from "./runDeveloperAgent"; 3 | 4 | dotenv.config(); 5 | 6 | const openAiApiKey = process.env.OPENAI_API_KEY; 7 | const task = process.argv.slice(2).join(" "); 8 | 9 | if (!openAiApiKey) { 10 | throw new Error("OPENAI_API_KEY is not set"); 11 | } 12 | 13 | // PROJECT INSTRUCTIONS 14 | // Specific to js-agent example. Adjust to fit your own project: 15 | // const projectInstructions = `You are working on a JavaScript/TypeScript project called "js-agent". 16 | // The project uses pnpm for package management. 17 | // The main package is located in the "packages/agent" directory. 18 | 19 | // Unit tests are written using jest and have a .test.ts ending. 20 | // Unit tests are in the same folder as the files that are tested. 21 | // When writing tests, first read the production code and then write the tests. 22 | // You can run the tests with "ai-bin/test-agent.sh".`; 23 | const projectInstructions = `You are working on a JavaScript/TypeScript project.`; 24 | 25 | // PROJECT SPECIFIC SETUP COMMANDS 26 | // These commands are executed at the start of the agent run. 27 | // Specific to js-agent example. Adjust to fit your own project: 28 | // const setupCommands = ["pnpm install", "pnpm nx run agent:build"]; 29 | const setupCommands: Array = []; 30 | 31 | runDeveloperAgent({ 32 | openAiApiKey, 33 | task, 34 | projectInstructions, 35 | setupCommands, 36 | }) 37 | .then(() => {}) 38 | .catch((error) => { 39 | console.error(error); 40 | }); 41 | -------------------------------------------------------------------------------- /examples/javascript-developer/src/runDeveloperAgent.ts: -------------------------------------------------------------------------------- 1 | import * as $ from "js-agent"; 2 | 3 | export async function runDeveloperAgent({ 4 | openAiApiKey, 5 | task, 6 | projectInstructions, 7 | setupCommands, 8 | }: { 9 | openAiApiKey: string; 10 | task: string; 11 | projectInstructions: string; 12 | setupCommands: string[]; 13 | }) { 14 | const model = $.provider.openai.chatModel({ 15 | apiKey: openAiApiKey, 16 | model: "gpt-4", 17 | }); 18 | 19 | const executeRemote = $.tool.executeRemoteTool({ 20 | baseUrl: "http://localhost:3001", 21 | }) as any; 22 | 23 | return $.runAgent({ 24 | properties: { task, projectInstructions }, 25 | agent: $.step.createFixedStepsLoop({ 26 | steps: [ 27 | $.step.createFixedStepsLoop({ 28 | type: "setup", 29 | steps: setupCommands.map( 30 | (command) => async (run) => 31 | $.step.createActionStep({ 32 | action: $.tool.runCommand({ execute: executeRemote }), 33 | input: { command }, 34 | run, 35 | }) 36 | ), 37 | }), 38 | $.step.generateNextStepLoop({ 39 | actions: [ 40 | $.tool.readFile({ execute: executeRemote }), 41 | $.tool.writeFile({ execute: executeRemote }), 42 | $.tool.runCommand({ execute: executeRemote }), 43 | $.tool.askUser({ 44 | execute: $.tool.executeAskUser(), 45 | }), 46 | ], 47 | actionFormat: $.action.format.json(), 48 | prompt: $.prompt.concatChatPrompts( 49 | $.prompt.sectionsChatPrompt({ 50 | role: "system", 51 | getSections: async ({ runState: { projectInstructions } }) => [ 52 | { 53 | title: "role", 54 | content: `You are a software developer that creates and modifies JavaScript programs. 55 | You are working in a Linux environment.`, 56 | }, 57 | { title: "project", content: projectInstructions }, 58 | { 59 | title: "constraints", 60 | content: `You must verify that the changes that you make are working.`, 61 | }, 62 | ], 63 | }), 64 | $.prompt.availableActionsChatPrompt(), 65 | $.prompt.sectionsChatPrompt({ 66 | role: "user", 67 | getSections: async ({ runState: { task } }) => [ 68 | { title: "Task", content: task }, 69 | ], 70 | }), 71 | $.prompt.recentStepsChatPrompt({ maxSteps: 10 }) 72 | ), 73 | model, 74 | }), 75 | ], 76 | }), 77 | observer: $.agent.observer.combineObservers( 78 | $.agent.observer.showRunInConsole({ name: "JavaScript Developer Agent" }), 79 | { 80 | async onRunFinished({ run }) { 81 | const runCostInMillicent = await $.agent.calculateRunCostInMillicent({ 82 | run, 83 | }); 84 | 85 | console.log( 86 | `Run cost: $${(runCostInMillicent / 1000 / 100).toFixed(2)}` 87 | ); 88 | 89 | console.log( 90 | `LLM calls: ${ 91 | run.recordedCalls.filter((call) => call.success).length 92 | }` 93 | ); 94 | }, 95 | } 96 | ), 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /examples/javascript-developer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "sourceMap": true, 6 | "target": "es2020", 7 | "lib": ["es2020", "dom"], 8 | "module": "commonjs", 9 | "types": ["node"], 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "rootDir": "./src", 14 | "outDir": "./.build/js" 15 | }, 16 | "include": ["src/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /examples/pdf-to-twitter-thread/README.md: -------------------------------------------------------------------------------- 1 | # PDF to Twitter Thread 2 | 3 | Takes a PDF and a topic and creates a Twitter thread with all content from the PDF that is relevant to the topic. 4 | 5 | ## JS Agent features used 6 | 7 | - Stand-alone load/extract/rewrite pipeline (no agent) 8 | - PDF loading 9 | - OpenAI chat completion model (`gpt-4`) 10 | 11 | ## Usage 12 | 13 | 1. Create .env file with the following content: 14 | 15 | ``` 16 | OPENAI_API_KEY="YOUR_OPENAI_API_KEY" 17 | ``` 18 | 19 | 2. Run the following commands: 20 | 21 | ```sh 22 | # in root folder: 23 | pnpm install 24 | pnpm nx run-many --target=build 25 | 26 | # in examples/pdf-summarizer folder: 27 | pnpm start -f my.pdf -t "my topic" 28 | ``` 29 | 30 | ## Example output 31 | 32 | ```bash 33 | ❯ pnpm start -f ~/Downloads/parnin_2012_crowd_documentation.pdf -t "android api" 34 | 35 | > @js-agent/example-pdf-summarizer@0.0.0 start /Users/lgrammel/repositories/js-agent/examples/pdf-summarizer 36 | > ts-node src/main.ts "-f" "/Users/lgrammel/Downloads/parnin_2012_crowd_documentation.pdf" "-t" "android api" 37 | 38 | ...extract-information 39 | ...extract-information 40 | ...extract-information 41 | ...extract-information 42 | ...extract-information 43 | ...extract-information 44 | ...extract-information 45 | ...extract-information 46 | ...extract-information 47 | ...extract-information 48 | ...extract-information 49 | ...extract-information 50 | ...extract-information 51 | ...extract-information 52 | ...extract-information 53 | ...extract-information 54 | ...rewrite-extracted-information 55 | 56 | 1/ 🧵 Excited to share our findings on the #AndroidAPI! We analyzed 6,323 questions & 10,638 answers on GWT API, 119,894 questions & 178,084 answers on Android API, and 181,560 questions & 445,934 answers on Java API. Here's what we discovered👇 57 | --- 58 | 2/ Crowd documentation generates numerous examples & explanations of API elements. While the crowd achieves high coverage, the speed is linear over time. Discussions involve the “crowd” asking questions & a smaller pool of “experts” answering them. #API #AndroidDev 59 | --- 60 | 3/ For 87% of all Android API classes, at least one thread on Stack Overflow was found. There's a strong correlation between usage data (from Google Code Search) & coverage data (from Stack Overflow) for Android, with a Spearman's rank correlation coefficient of 0.797. 📊 61 | --- 62 | 4/ Popular packages like android.widget & android.view are well covered, while areas like android.drm (digital rights management) & android.accessibilityservice (accessibility) are largely ignored by the crowd. #AndroidAPI #StackOverflow 63 | --- 64 | 5/ For all three APIs (Android, Java, and GWT), the rate at which new classes are covered by the crowd follows a linear pattern. API designers can't completely rely on the crowd to provide Q&A for an entire API. Some areas, like accessibility, are ignored. #APIs #AndroidDev 65 | --- 66 | 6/ Check out the interactive treemap visualization for Android API at http://latest-print.crowd-documentation.appspot.com/?api=android. It helps researchers, API designers, and users understand their community and visualize contributions. #AndroidAPI #DataVisualization 🌐 67 | ``` 68 | -------------------------------------------------------------------------------- /examples/pdf-to-twitter-thread/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-agent/example-pdf-summarizer", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "ts-node src/main.ts", 7 | "build": "tsc" 8 | }, 9 | "private": true, 10 | "dependencies": { 11 | "js-agent": "*", 12 | "dotenv": "16.0.3", 13 | "commander": "10.0.1" 14 | }, 15 | "devDependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /examples/pdf-to-twitter-thread/src/createTwitterThreadFromPdf.ts: -------------------------------------------------------------------------------- 1 | import * as $ from "js-agent"; 2 | 3 | export async function createTwitterThreadFromPdf({ 4 | topic, 5 | pdfPath, 6 | openAiApiKey, 7 | context, 8 | }: { 9 | topic: string; 10 | pdfPath: string; 11 | openAiApiKey: string; 12 | context: $.agent.RunContext; 13 | }) { 14 | const gpt4 = $.provider.openai.chatModel({ 15 | apiKey: openAiApiKey, 16 | model: "gpt-4", 17 | }); 18 | 19 | const rewriteAsTwitterThread = $.text.splitExtractRewrite.asExtractFunction({ 20 | split: $.text.splitRecursivelyAtCharacter.asSplitFunction({ 21 | maxChunkSize: 1024 * 4, 22 | }), 23 | extract: $.text.generateText.asFunction({ 24 | id: "extract", 25 | model: gpt4, 26 | prompt: $.prompt.extractAndExcludeChatPrompt({ 27 | excludeKeyword: "IRRELEVANT", 28 | }), 29 | retry: $.util.retryWithExponentialBackoff({ 30 | maxTries: 5, 31 | delay: 4000, 32 | }), 33 | }), 34 | include: (text) => text !== "IRRELEVANT", 35 | rewrite: $.text.generateText.asFunction({ 36 | id: "rewrite", 37 | model: gpt4, 38 | prompt: async ({ text, topic }) => [ 39 | { 40 | role: "user" as const, 41 | content: `## TOPIC\n${topic}`, 42 | }, 43 | { 44 | role: "system" as const, 45 | content: `## TASK 46 | Rewrite the content below into a coherent twitter thread on the topic above. 47 | Include all relevant information about the topic. 48 | Discard all irrelevant information. 49 | Separate each tweet with ---`, 50 | }, 51 | { 52 | role: "user" as const, 53 | content: `## CONTENT\n${text}`, 54 | }, 55 | ], 56 | }), 57 | }); 58 | 59 | return rewriteAsTwitterThread( 60 | { 61 | text: await $.text.load({ 62 | from: { path: pdfPath }, 63 | using: $.source.fileAsArrayBuffer.asFunction(), 64 | convert: $.convert.pdfToText.asFunction(), 65 | }), 66 | topic, 67 | }, 68 | context 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /examples/pdf-to-twitter-thread/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "commander"; 2 | import dotenv from "dotenv"; 3 | import { createTwitterThreadFromPdf } from "./createTwitterThreadFromPdf"; 4 | 5 | dotenv.config(); 6 | 7 | const program = new Command(); 8 | 9 | program 10 | .description("PDF summarizer") 11 | .requiredOption("-f, --file ", "Path to PDF file") 12 | .requiredOption("-t, --topic ", "Topic") 13 | .parse(process.argv); 14 | 15 | const { file, topic } = program.opts(); 16 | 17 | const openAiApiKey = process.env.OPENAI_API_KEY; 18 | 19 | if (!openAiApiKey) { 20 | throw new Error("OPENAI_API_KEY is not set"); 21 | } 22 | 23 | createTwitterThreadFromPdf({ 24 | topic, 25 | pdfPath: file, 26 | openAiApiKey, 27 | context: { 28 | recordCall: (call) => { 29 | console.log(`${call.metadata.id ?? "unknown"}...`); 30 | }, 31 | }, 32 | }) 33 | .then((result) => { 34 | console.log(); 35 | console.log(result); 36 | }) 37 | .catch((error) => { 38 | console.error(error); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/pdf-to-twitter-thread/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "sourceMap": true, 6 | "target": "es2020", 7 | "lib": ["es2020", "dom"], 8 | "module": "commonjs", 9 | "types": ["node"], 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "rootDir": "./src", 14 | "outDir": "./.build/js" 15 | }, 16 | "include": ["src/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /examples/split-and-embed-text/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-agent/example-split-and-embed", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "ts-node src/main.ts", 7 | "build": "tsc" 8 | }, 9 | "private": true, 10 | "dependencies": { 11 | "js-agent": "*", 12 | "dotenv": "16.0.3", 13 | "commander": "10.0.1" 14 | }, 15 | "devDependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /examples/split-and-embed-text/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "commander"; 2 | import dotenv from "dotenv"; 3 | import { splitAndEmbedText } from "./splitAndEmbedText"; 4 | 5 | dotenv.config(); 6 | 7 | const program = new Command(); 8 | 9 | program 10 | .description("PDF summarizer") 11 | .requiredOption("-f, --file ", "Path to text file") 12 | .parse(process.argv); 13 | 14 | const { file } = program.opts(); 15 | 16 | const openAiApiKey = process.env.OPENAI_API_KEY; 17 | 18 | if (!openAiApiKey) { 19 | throw new Error("OPENAI_API_KEY is not set"); 20 | } 21 | 22 | splitAndEmbedText({ 23 | textFilePath: file, 24 | openAiApiKey, 25 | }) 26 | .then((result) => {}) 27 | .catch((error) => { 28 | console.error(error); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/split-and-embed-text/src/splitAndEmbedText.ts: -------------------------------------------------------------------------------- 1 | import * as $ from "js-agent"; 2 | import fs from "node:fs/promises"; 3 | 4 | const openai = $.provider.openai; 5 | 6 | export async function splitAndEmbedText({ 7 | textFilePath, 8 | openAiApiKey, 9 | }: { 10 | textFilePath: string; 11 | openAiApiKey: string; 12 | }) { 13 | const text = await fs.readFile(textFilePath, "utf8"); 14 | 15 | const chunks = await $.text.splitRecursivelyAtToken({ 16 | text, 17 | tokenizer: openai.tokenizer.forModel({ 18 | model: "text-embedding-ada-002", 19 | }), 20 | maxChunkSize: 128, 21 | }); 22 | 23 | const embeddings = []; 24 | for (const chunk of chunks) { 25 | const response = await openai.api.generateEmbedding({ 26 | model: "text-embedding-ada-002", 27 | apiKey: openAiApiKey, 28 | input: chunk, 29 | }); 30 | 31 | embeddings.push({ 32 | chunk, 33 | embedding: response.data[0].embedding, 34 | }); 35 | } 36 | 37 | console.log(embeddings); 38 | } 39 | -------------------------------------------------------------------------------- /examples/split-and-embed-text/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "sourceMap": true, 6 | "target": "es2020", 7 | "lib": ["es2020", "dom"], 8 | "module": "commonjs", 9 | "types": ["node"], 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "rootDir": "./src", 14 | "outDir": "./.build/js" 15 | }, 16 | "include": ["src/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /examples/wikipedia/README.md: -------------------------------------------------------------------------------- 1 | # JS Agent Wikipedia 2 | 3 | Answers questions using Wikipedia articles. It searches using a Programmable Search Engine set up for en.wikipedia.org and reads (summarizes) articles to find the answer. 4 | 5 | [Full tutorial](https://js-agent.ai/docs/tutorial-wikipedia-agent/) 6 | 7 | ## JS Agent features used 8 | 9 | - OpenAI chat completion model (`gpt-3.5-turbo`) 10 | - Custom tool configuration (`readWikipediaArticleAction`, `searchWikipediaAction`) 11 | - `GenerateNextStepLoop` loop with tools and custom prompt 12 | - `maxSteps` `RunController` to limit the maximum number of steps 13 | 14 | ## Usage 15 | 16 | 1. Create a [Programmable Search Engine](https://programmablesearchengine.google.com/about/) for en.wikipedia.org and get the key and cx. 17 | 18 | 2. Create .env file with the following content: 19 | 20 | ``` 21 | WIKIPEDIA_SEARCH_KEY="YOUR_CUSTOM_SEARCH_KEY" 22 | WIKIPEDIA_SEARCH_CX="YOUR_CUSTOM_SEARCH_CX" 23 | OPENAI_API_KEY="YOUR_OPENAI_API_KEY" 24 | ``` 25 | 26 | 3. Run the following commands: 27 | 28 | ```sh 29 | # in root folder: 30 | pnpm install 31 | pnpm nx run-many --target=build 32 | 33 | # in examples/wikipedia folder: 34 | pnpm start "which town has more inhabitants, Ladysmith or Duncan BC?" 35 | ``` 36 | -------------------------------------------------------------------------------- /examples/wikipedia/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-agent/example-wikipedia", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "ts-node src/main.ts", 7 | "build": "tsc" 8 | }, 9 | "private": true, 10 | "dependencies": { 11 | "js-agent": "*", 12 | "dotenv": "16.0.3" 13 | }, 14 | "devDependencies": {} 15 | } 16 | -------------------------------------------------------------------------------- /examples/wikipedia/screenshot/wikipedia-qa-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/js-agent/bc85c63380a61ff8e773332832f93b27271d3328/examples/wikipedia/screenshot/wikipedia-qa-001.png -------------------------------------------------------------------------------- /examples/wikipedia/src/main.ts: -------------------------------------------------------------------------------- 1 | import { runWikipediaAgent } from "./runWikipediaAgent"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | const wikipediaSearchKey = process.env.WIKIPEDIA_SEARCH_KEY; 7 | const wikipediaSearchCx = process.env.WIKIPEDIA_SEARCH_CX; 8 | const openAiApiKey = process.env.OPENAI_API_KEY; 9 | const task = process.argv.slice(2).join(" "); 10 | 11 | if (!wikipediaSearchKey) { 12 | throw new Error("WIKIPEDIA_SEARCH_KEY is not set"); 13 | } 14 | if (!wikipediaSearchCx) { 15 | throw new Error("WIKIPEDIA_SEARCH_CX is not set"); 16 | } 17 | if (!openAiApiKey) { 18 | throw new Error("OPENAI_API_KEY is not set"); 19 | } 20 | 21 | runWikipediaAgent({ 22 | wikipediaSearchCx, 23 | wikipediaSearchKey, 24 | openAiApiKey, 25 | task, 26 | }) 27 | .then(() => {}) 28 | .catch((error) => { 29 | console.error(error); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/wikipedia/src/runWikipediaAgent.ts: -------------------------------------------------------------------------------- 1 | import * as $ from "js-agent"; 2 | 3 | const openai = $.provider.openai; 4 | 5 | export async function runWikipediaAgent({ 6 | wikipediaSearchKey, 7 | wikipediaSearchCx, 8 | openAiApiKey, 9 | task, 10 | }: { 11 | openAiApiKey: string; 12 | wikipediaSearchKey: string; 13 | wikipediaSearchCx: string; 14 | task: string; 15 | }) { 16 | const searchWikipediaAction = $.tool.programmableGoogleSearchEngineAction({ 17 | id: "search-wikipedia", 18 | description: 19 | "Search wikipedia using a search term. Returns a list of pages.", 20 | execute: $.tool.executeProgrammableGoogleSearchEngineAction({ 21 | key: wikipediaSearchKey, 22 | cx: wikipediaSearchCx, 23 | }), 24 | }); 25 | 26 | const readWikipediaArticleAction = $.tool.extractInformationFromWebpage({ 27 | id: "read-wikipedia-article", 28 | description: 29 | "Read a wikipedia article and summarize it considering the query.", 30 | inputExample: { 31 | url: "https://en.wikipedia.org/wiki/Artificial_intelligence", 32 | topic: "{query that you are answering}", 33 | }, 34 | execute: $.tool.executeExtractInformationFromWebpage({ 35 | extract: $.text.extractRecursively.asExtractFunction({ 36 | split: $.text.splitRecursivelyAtToken.asSplitFunction({ 37 | tokenizer: openai.tokenizer.forModel({ 38 | model: "gpt-3.5-turbo", 39 | }), 40 | maxChunkSize: 2048, // needs to fit into a gpt-3.5-turbo prompt and leave room for the answer 41 | }), 42 | extract: $.text.generateText.asFunction({ 43 | prompt: $.prompt.extractChatPrompt(), 44 | model: openai.chatModel({ 45 | apiKey: openAiApiKey, 46 | model: "gpt-3.5-turbo", 47 | }), 48 | }), 49 | }), 50 | }), 51 | }); 52 | 53 | return $.runAgent<{ task: string }>({ 54 | properties: { task }, 55 | agent: $.step.generateNextStepLoop({ 56 | actions: [searchWikipediaAction, readWikipediaArticleAction], 57 | actionFormat: $.action.format.flexibleJson(), 58 | prompt: $.prompt.concatChatPrompts( 59 | async ({ runState: { task } }) => [ 60 | { 61 | role: "system", 62 | content: `## ROLE 63 | You are an knowledge worker that answers questions using Wikipedia content. You speak perfect JSON. 64 | 65 | ## CONSTRAINTS 66 | All facts for your answer must be from Wikipedia articles that you have read. 67 | 68 | ## TASK 69 | ${task}`, 70 | }, 71 | ], 72 | $.prompt.availableActionsChatPrompt(), 73 | $.prompt.recentStepsChatPrompt({ maxSteps: 6 }) 74 | ), 75 | model: openai.chatModel({ 76 | apiKey: openAiApiKey, 77 | model: "gpt-3.5-turbo", 78 | }), 79 | }), 80 | controller: $.agent.controller.maxSteps(20), 81 | observer: $.agent.observer.showRunInConsole({ name: "Wikipedia Agent" }), 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /examples/wikipedia/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "sourceMap": true, 6 | "target": "es2020", 7 | "lib": ["es2020", "dom"], 8 | "module": "commonjs", 9 | "types": ["node"], 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "rootDir": "./src", 14 | "outDir": "./.build/js" 15 | }, 16 | "include": ["src/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-agent-repo", 3 | "version": "0.0.1", 4 | "private": "true", 5 | "description": "", 6 | "scripts": { 7 | "start": "ts-node src/index.ts", 8 | "build": "tsc" 9 | }, 10 | "keywords": [], 11 | "author": "Lars Grammel", 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "@types/jest": "29.5.0", 15 | "@types/node": "18.15.3", 16 | "esbuild": "0.17.12", 17 | "nx": "15.8.7", 18 | "ts-node": "10.9.1", 19 | "typescript": "5.0.2", 20 | "ts-jest": "29.1.0", 21 | "jest": "29.5.0" 22 | }, 23 | "workspaces": [ 24 | "packages/*" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/agent/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lars Grammel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/agent/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | }; 6 | -------------------------------------------------------------------------------- /packages/agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-agent", 3 | "description": "Build AI Agents & Apps with JS & TS", 4 | "version": "0.0.24", 5 | "homepage": "https://github.com/lgrammel/js-agent", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/lgrammel/js-agent.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/lgrammel/js-agent/issues" 12 | }, 13 | "keywords": [ 14 | "llm", 15 | "gpt3", 16 | "gpt4", 17 | "agent", 18 | "autogpt", 19 | "babyagi", 20 | "openai" 21 | ], 22 | "license": "MIT", 23 | "engines": { 24 | "node": ">=18" 25 | }, 26 | "main": ".build/js/index.js", 27 | "bin": { 28 | "js-agent-server": ".build/js/server/startAgentServer.js" 29 | }, 30 | "scripts": { 31 | "test": "jest src/" 32 | }, 33 | "devDependencies": { 34 | "@types/accepts": "^1.3.5", 35 | "@types/html-to-text": "^9.0.0", 36 | "@types/ws": "^8.5.4" 37 | }, 38 | "dependencies": { 39 | "@dqbd/tiktoken": "1.0.7", 40 | "@fastify/accepts": "4.1.0", 41 | "@fastify/websocket": "7.2.0", 42 | "axios": "1.3.4", 43 | "chalk": "^4", 44 | "commander": "10.0.1", 45 | "dotenv": "16.0.3", 46 | "fastify": "4.14.1", 47 | "fastify-type-provider-zod": "1.1.9", 48 | "html-to-text": "9.0.5", 49 | "hyperid": "3.1.1", 50 | "pdfjs-dist": "3.5.141", 51 | "pino": "8.11.0", 52 | "pino-pretty": "10.0.0", 53 | "secure-json-parse": "2.7.0", 54 | "zod": "3.21.4" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/agent/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "packages/agent", 3 | "targets": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "executor": "nx:run-commands", 7 | "options": { 8 | "cwd": "packages/agent", 9 | "command": "tsc" 10 | } 11 | }, 12 | "test": { 13 | "dependsOn": ["build"], 14 | "executor": "nx:run-commands", 15 | "options": { 16 | "cwd": "packages/agent", 17 | "command": "pnpm test" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/agent/src/action/Action.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | import { Run } from "../agent/Run"; 3 | import { Step } from "../step/Step"; 4 | import { ActionParameters } from "./ActionParameters"; 5 | import { ExecuteBasicToolFunction } from "./ExecuteBasicToolFunction"; 6 | import { ExecuteReflectiveToolFunction } from "./ExecuteReflectiveToolFunction"; 7 | import { FormatResultFunction } from "./FormatResultFunction"; 8 | 9 | export type AnyAction = Action; 10 | 11 | export type Action = 12 | | CustomStepAction 13 | | BasicToolAction 14 | | ReflectiveToolAction; 15 | 16 | export type BaseAction = { 17 | readonly id: string; 18 | readonly description: string; 19 | 20 | readonly inputSchema: zod.Schema; 21 | readonly inputExample?: INPUT; 22 | 23 | readonly outputSchema: zod.Schema; 24 | }; 25 | 26 | export type CustomStepAction< 27 | INPUT extends ActionParameters, 28 | OUTPUT, 29 | RUN_STATE 30 | > = BaseAction & { 31 | readonly type: "custom-step"; 32 | createStep: (options: { 33 | input: INPUT; 34 | run: Run; 35 | }) => PromiseLike>; 36 | }; 37 | 38 | export type BasicToolAction< 39 | INPUT extends ActionParameters, 40 | OUTPUT 41 | > = BaseAction & { 42 | readonly type: "basic-tool"; 43 | execute: ExecuteBasicToolFunction; 44 | formatResult: FormatResultFunction; 45 | }; 46 | 47 | export type ReflectiveToolAction< 48 | INPUT extends ActionParameters, 49 | OUTPUT, 50 | RUN_STATE 51 | > = BaseAction & { 52 | readonly type: "reflective-tool"; 53 | execute: ExecuteReflectiveToolFunction; 54 | formatResult: FormatResultFunction; 55 | }; 56 | -------------------------------------------------------------------------------- /packages/agent/src/action/ActionParameters.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | 3 | export type ActionParameters = Record & { 4 | action?: string; 5 | _freeText?: string; 6 | }; 7 | 8 | export const actionParametersSchema = zod.record( 9 | zod.union([zod.string(), zod.undefined()]) 10 | ); 11 | -------------------------------------------------------------------------------- /packages/agent/src/action/ActionRegistry.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "./Action"; 2 | import { done } from "./done"; 3 | import { ActionFormat } from "./format/ActionFormat"; 4 | 5 | export class ActionRegistry { 6 | readonly format: ActionFormat; 7 | readonly doneAction: AnyAction; 8 | 9 | private readonly actions: Map> = new Map(); 10 | 11 | constructor({ 12 | actions, 13 | doneAction = done(), 14 | format, 15 | }: { 16 | actions: AnyAction[]; 17 | doneAction?: AnyAction; 18 | format: ActionFormat; 19 | }) { 20 | for (const action of actions) { 21 | this.register(action); 22 | } 23 | 24 | this.doneAction = doneAction; 25 | this.format = format; 26 | } 27 | 28 | register(action: AnyAction) { 29 | if (this.actions.has(action.id)) { 30 | throw new Error( 31 | `An action with the name '${action.id}' has already been registered.` 32 | ); 33 | } 34 | 35 | this.actions.set(action.id, action); 36 | } 37 | 38 | getAction(type: string) { 39 | const action = this.actions.get(type); 40 | 41 | if (action == null && type === this.doneAction.id) { 42 | return this.doneAction; 43 | } 44 | 45 | if (!action) { 46 | throw new Error( 47 | `No action with the type '${type}' has been registered. ${this.availableActionTypesMessage}` 48 | ); 49 | } 50 | 51 | return action; 52 | } 53 | 54 | getAvailableActionInstructions() { 55 | return `You can perform the following actions using ${ 56 | this.format.description 57 | }: 58 | 59 | ${this.describeActions()} 60 | 61 | ## RESPONSE FORMAT (ALWAYS USE THIS FORMAT) 62 | 63 | Explain and describe your reasoning step by step. 64 | Then use the following format to specify the action you want to perform next: 65 | 66 | ${this.format.format({ 67 | action: "an action", 68 | param1: "a parameter value", 69 | param2: "another parameter value", 70 | })} 71 | 72 | You must always use exactly one action with the correct syntax per response. 73 | Each response must precisely follow the action syntax.`; 74 | } 75 | 76 | private get availableActionTypesMessage() { 77 | return `Available actions: ${this.actionTypes.join(", ")}`; 78 | } 79 | 80 | get actionTypes() { 81 | return [Array.from(this.actions.keys()), this.doneAction.id].flat(); 82 | } 83 | 84 | describeActions() { 85 | return [...Array.from(this.actions.values()), this.doneAction] 86 | .map( 87 | (action) => 88 | `### ${action.id}\n${ 89 | action.description 90 | }\nSyntax:\n${this.format.format( 91 | Object.assign( 92 | { 93 | action: action.id, 94 | }, 95 | action.inputExample 96 | ) 97 | )}` 98 | ) 99 | .join("\n\n"); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/agent/src/action/ExecuteBasicToolFunction.ts: -------------------------------------------------------------------------------- 1 | import { BasicToolAction } from "./Action"; 2 | import { ActionParameters } from "./ActionParameters"; 3 | import { RunContext } from "../agent/RunContext"; 4 | 5 | export type ExecuteBasicToolFunction = ( 6 | {}: { 7 | input: INPUT; 8 | action: BasicToolAction; 9 | }, 10 | context: RunContext 11 | ) => Promise<{ output: OUTPUT; summary: string }>; 12 | -------------------------------------------------------------------------------- /packages/agent/src/action/ExecuteReflectiveToolFunction.ts: -------------------------------------------------------------------------------- 1 | import { ReflectiveToolAction } from "../action/Action"; 2 | import { ActionParameters } from "../action/ActionParameters"; 3 | import { Run } from "../agent"; 4 | import { RunContext } from "../agent/RunContext"; 5 | 6 | export type ExecuteReflectiveToolFunction< 7 | INPUT extends ActionParameters, 8 | OUTPUT, 9 | RUN_STATE 10 | > = ( 11 | options: { 12 | input: INPUT; 13 | action: ReflectiveToolAction; 14 | run: Run; 15 | }, 16 | context: RunContext 17 | ) => Promise<{ output: OUTPUT; summary: string }>; 18 | -------------------------------------------------------------------------------- /packages/agent/src/action/FormatResultFunction.ts: -------------------------------------------------------------------------------- 1 | import { ActionParameters } from "./ActionParameters"; 2 | 3 | export type FormatResultFunction = ({ 4 | input, 5 | summary, 6 | output, 7 | }: { 8 | input: INPUT; 9 | summary: string; 10 | output: OUTPUT; 11 | }) => string; 12 | -------------------------------------------------------------------------------- /packages/agent/src/action/done.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | import { CustomStepAction } from "./Action"; 3 | import { NoopStep } from "../step/NoopStep"; 4 | 5 | type DoneActionInput = { 6 | _freeText?: string; 7 | result: string; 8 | }; 9 | 10 | export const done = ({ 11 | id = "done", 12 | description = "Indicate that you are done with the task.", 13 | inputExample = { 14 | result: "{result of the task}", 15 | }, 16 | }: { 17 | id?: string; 18 | description?: string; 19 | inputExample?: DoneActionInput; 20 | } = {}): CustomStepAction => ({ 21 | type: "custom-step", 22 | id, 23 | description, 24 | inputSchema: zod.object({ 25 | result: zod.string(), 26 | }), 27 | inputExample, 28 | outputSchema: zod.object({}), 29 | createStep: async ({ input: { result }, run }) => { 30 | return new NoopStep({ 31 | type: id, 32 | run, 33 | summary: result, 34 | isDoneStep: true, 35 | }); 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /packages/agent/src/action/format/ActionFormat.ts: -------------------------------------------------------------------------------- 1 | import { ActionParameters } from "../ActionParameters"; 2 | 3 | /** 4 | * Format for parsing/formatting actions from text. 5 | * 6 | * This is used to instruct the LLM how to execute actions and to parse the actions that the LLM wants to execute. 7 | */ 8 | export type ActionFormat = { 9 | /** 10 | * A description of the action format. 11 | */ 12 | description: string | undefined; 13 | 14 | /** 15 | * Formats the given action parameters into a string. 16 | * 17 | * @param parameters - The action parameters to format. 18 | * @returns The formatted string. 19 | */ 20 | format(parameters: ActionParameters): string; 21 | 22 | /** 23 | * Parses the given text into action parameters. 24 | * 25 | * @param text - The text to parse. 26 | * @returns The parsed action parameters. 27 | */ 28 | parse(text: string): ActionParameters; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/agent/src/action/format/flexibleJson.ts: -------------------------------------------------------------------------------- 1 | import { ActionParameters, actionParametersSchema } from "../ActionParameters"; 2 | import { ActionFormat } from "./ActionFormat"; 3 | import SecureJSON from "secure-json-parse"; 4 | 5 | /** 6 | * Parses the first JSON object that it 7 | * finds in the response and is better suited for `gpt-3.5-turbo`, which does not 8 | * reliably insert the JSON object at the end of the response. 9 | */ 10 | export const flexibleJson = (): ActionFormat => ({ 11 | description: "JSON", 12 | 13 | format(parameters: ActionParameters): string { 14 | return JSON.stringify(parameters, null, 2); 15 | }, 16 | 17 | parse(text: string): ActionParameters { 18 | const [jsonObject, freeText] = extractFirstSingleLevelJsonObject(text); 19 | 20 | if (jsonObject == null) { 21 | return { _freeText: freeText }; 22 | } 23 | 24 | try { 25 | return { 26 | ...actionParametersSchema.parse(jsonObject), 27 | _freeText: freeText.trim(), 28 | }; 29 | } catch (error: any) { 30 | throw new Error( 31 | `${text} could not be parsed as JSON: ${error?.message ?? error}` 32 | ); 33 | } 34 | }, 35 | }); 36 | 37 | function extractFirstSingleLevelJsonObject( 38 | text: string 39 | ): [object | null, string] { 40 | const jsonStartIndex = text.indexOf("{"); 41 | 42 | // assumes no nested objects: 43 | const jsonEndIndex = text.indexOf("}", jsonStartIndex); 44 | 45 | if ( 46 | jsonStartIndex === -1 || 47 | jsonEndIndex === -1 || 48 | jsonStartIndex > jsonEndIndex 49 | ) { 50 | return [null, text]; 51 | } 52 | 53 | const jsonString = text.slice(jsonStartIndex, jsonEndIndex + 1); 54 | let jsonObject: object | null = null; 55 | 56 | try { 57 | jsonObject = SecureJSON.parse(jsonString); 58 | } catch (error) { 59 | return [null, text]; 60 | } 61 | 62 | const textBeforeJson = text.slice(0, jsonStartIndex); 63 | const textAfterJson = text.slice(jsonEndIndex + 1); 64 | 65 | return [jsonObject, textBeforeJson + textAfterJson]; 66 | } 67 | -------------------------------------------------------------------------------- /packages/agent/src/action/format/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ActionFormat"; 2 | export * from "./flexibleJson"; 3 | export * from "./json"; 4 | -------------------------------------------------------------------------------- /packages/agent/src/action/format/json.ts: -------------------------------------------------------------------------------- 1 | import { ActionParameters, actionParametersSchema } from "../ActionParameters"; 2 | import { ActionFormat } from "./ActionFormat"; 3 | import SecureJSON from "secure-json-parse"; 4 | 5 | export const json = (): ActionFormat => ({ 6 | /** 7 | * A description of the JSON action format. 8 | */ 9 | description: "JSON", 10 | 11 | /** 12 | * Formats the given action parameters into a JSON string. 13 | * 14 | * @param parameters - The action parameters to format. 15 | * @returns The formatted JSON string. 16 | */ 17 | format(parameters: ActionParameters): string { 18 | return JSON.stringify(parameters, null, 2); 19 | }, 20 | 21 | /** 22 | * Parses the given text into action parameters, handling JSON objects and free text. 23 | * 24 | * @param text - The text to parse. 25 | * @returns The parsed action parameters. 26 | */ 27 | parse(text: string): ActionParameters { 28 | if (!text.trim().endsWith("}")) { 29 | return { _freeText: text }; 30 | } 31 | 32 | try { 33 | const firstOpeningBraceIndex = text.indexOf("{"); 34 | const freeText = text.slice(0, firstOpeningBraceIndex); 35 | const jsonText = text.slice(firstOpeningBraceIndex); 36 | const jsonObject = SecureJSON.parse(jsonText); 37 | 38 | return { 39 | ...actionParametersSchema.parse(jsonObject), 40 | _freeText: freeText.trim(), 41 | }; 42 | } catch (error: any) { 43 | throw new Error( 44 | `${text} could not be parsed as JSON: ${error?.message ?? error}` 45 | ); 46 | } 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /packages/agent/src/action/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Action.js"; 2 | export * from "./ActionParameters.js"; 3 | export * from "./ActionRegistry.js"; 4 | export * from "./done.js"; 5 | export * from "./FormatResultFunction.js"; 6 | export * as format from "./format/index.js"; 7 | -------------------------------------------------------------------------------- /packages/agent/src/agent/EmbedCall.ts: -------------------------------------------------------------------------------- 1 | export type EmbedCall = { 2 | type: "embed"; 3 | input: unknown; 4 | metadata: { 5 | id?: string | undefined; 6 | model: { 7 | vendor: string; 8 | name: string; 9 | }; 10 | startEpochSeconds: number; 11 | durationInMs: number; 12 | tries: number; 13 | }; 14 | } & ( 15 | | { success: true; rawOutput: unknown; embedding: unknown } 16 | | { success: false; error: unknown } 17 | ); 18 | -------------------------------------------------------------------------------- /packages/agent/src/agent/GenerateCall.ts: -------------------------------------------------------------------------------- 1 | export type GenerateCall = { 2 | type: "generate"; 3 | input: unknown; 4 | metadata: { 5 | id?: string | undefined; 6 | model: { 7 | vendor: string; 8 | name: string; 9 | }; 10 | startEpochSeconds: number; 11 | durationInMs: number; 12 | tries: number; 13 | }; 14 | } & ( 15 | | { success: true; rawOutput: unknown; extractedOutput: unknown } 16 | | { success: false; error: unknown } 17 | ); 18 | -------------------------------------------------------------------------------- /packages/agent/src/agent/Run.ts: -------------------------------------------------------------------------------- 1 | import { Loop } from "../step/Loop"; 2 | import { Step } from "../step/Step"; 3 | import { StepResult } from "../step/StepResult"; 4 | import { createNextId } from "../util/createNextId"; 5 | import { EmbedCall } from "./EmbedCall"; 6 | import { GenerateCall } from "./GenerateCall"; 7 | import { RunController } from "./controller/RunController"; 8 | import { RunObserver } from "./observer/RunObserver"; 9 | 10 | export type PrimitiveRecord = Record; 11 | 12 | export class Run { 13 | private readonly observer?: RunObserver; 14 | private readonly nextId = createNextId(1); 15 | 16 | readonly controller: RunController; 17 | readonly state: RUN_STATE; 18 | 19 | readonly recordedCalls: Array = []; 20 | 21 | root: Step | undefined; 22 | 23 | constructor({ 24 | controller, 25 | observer, 26 | initialState, 27 | }: { 28 | controller: RunController; 29 | observer?: RunObserver; 30 | initialState: RUN_STATE; 31 | }) { 32 | this.controller = controller; 33 | this.observer = observer; 34 | this.state = initialState; 35 | } 36 | 37 | generateId({ type }: { type: string }) { 38 | return `${this.nextId()}-${type}`; 39 | } 40 | 41 | checkCancel() { 42 | return this.controller.checkCancel(this); 43 | } 44 | 45 | private logError(error: unknown) { 46 | console.error(error); // TODO logger 47 | } 48 | 49 | onLoopIterationStarted({ loop }: { loop: Loop }) { 50 | try { 51 | this.observer?.onLoopIterationStarted?.({ run: this, loop }); 52 | } catch (error) { 53 | this.logError(error); 54 | } 55 | } 56 | 57 | onLoopIterationFinished({ loop }: { loop: Loop }) { 58 | try { 59 | this.observer?.onLoopIterationFinished?.({ run: this, loop }); 60 | } catch (error) { 61 | this.logError(error); 62 | } 63 | } 64 | 65 | onStepExecutionStarted({ step }: { step: Step }) { 66 | try { 67 | this.observer?.onStepExecutionStarted?.({ run: this, step }); 68 | } catch (error) { 69 | this.logError(error); 70 | } 71 | } 72 | 73 | onStepExecutionFinished({ 74 | step, 75 | result, 76 | }: { 77 | step: Step; 78 | result: StepResult; 79 | }) { 80 | try { 81 | this.observer?.onStepExecutionFinished?.({ run: this, step, result }); 82 | } catch (error) { 83 | this.logError(error); 84 | } 85 | } 86 | 87 | onStart() { 88 | try { 89 | this.observer?.onRunStarted?.({ run: this }); 90 | } catch (error) { 91 | this.logError(error); 92 | } 93 | } 94 | 95 | onFinish({ result }: { result: StepResult }) { 96 | try { 97 | this.observer?.onRunFinished?.({ run: this, result }); 98 | } catch (error) { 99 | this.logError(error); 100 | } 101 | } 102 | 103 | onStepGenerationStarted() { 104 | try { 105 | this.observer?.onStepGenerationStarted?.({ run: this }); 106 | } catch (error: any) { 107 | this.logError(error); 108 | } 109 | } 110 | 111 | onStepGenerationFinished({ 112 | generatedText, 113 | step, 114 | }: { 115 | generatedText: string; 116 | step: Step; 117 | }) { 118 | try { 119 | this.observer?.onStepGenerationFinished?.({ 120 | run: this, 121 | generatedText, 122 | step, 123 | }); 124 | } catch (error: any) { 125 | this.logError(error); 126 | } 127 | } 128 | 129 | recordCall(call: GenerateCall | EmbedCall) { 130 | // TODO associate with currently active step 131 | this.recordedCalls.push(call); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /packages/agent/src/agent/RunContext.ts: -------------------------------------------------------------------------------- 1 | import { EmbedCall } from "./EmbedCall"; 2 | import { GenerateCall } from "./GenerateCall"; 3 | 4 | export type RunContext = { 5 | recordCall: null | ((call: GenerateCall | EmbedCall) => void); 6 | } | null; 7 | -------------------------------------------------------------------------------- /packages/agent/src/agent/calculateRunCostInMillicent.ts: -------------------------------------------------------------------------------- 1 | import { calculateOpenAICallCostInMillicent } from "../provider/openai/cost/calculateOpenAICallCostInMillicent"; 2 | import { Run } from "./Run"; 3 | 4 | export const calculateRunCostInMillicent = async ({ 5 | run, 6 | }: { 7 | run: Run; 8 | }) => { 9 | const callCostsInMillicent = run.recordedCalls.map((call) => { 10 | if (call.success && call.metadata.model.vendor === "openai") { 11 | return calculateOpenAICallCostInMillicent(call); 12 | } 13 | return undefined; 14 | }); 15 | 16 | return callCostsInMillicent.reduce( 17 | (sum: number, cost) => sum + (cost ?? 0), 18 | 0 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/agent/src/agent/controller/RunController.ts: -------------------------------------------------------------------------------- 1 | import { Run } from ".."; 2 | 3 | export type RunController = { 4 | checkCancel( 5 | run: Run 6 | ): { shouldCancel: false } | { shouldCancel: true; reason?: string }; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/agent/src/agent/controller/all.ts: -------------------------------------------------------------------------------- 1 | import { Run } from ".."; 2 | import { RunController } from "./RunController"; 3 | 4 | export const all = ( 5 | ...controllers: RunController[] 6 | ): RunController => ({ 7 | checkCancel(run: Run) { 8 | for (const controller of controllers) { 9 | const check = controller.checkCancel(run); 10 | 11 | if (check.shouldCancel) { 12 | return check; 13 | } 14 | } 15 | 16 | return { shouldCancel: false as const }; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/agent/src/agent/controller/cancellable.ts: -------------------------------------------------------------------------------- 1 | import { Run } from ".."; 2 | import { RunController } from "./RunController"; 3 | 4 | export const cancellable = (): RunController & { 5 | cancel(options: { reason: string }): void; 6 | } => { 7 | let cancelReason: string | undefined = undefined; 8 | 9 | return { 10 | cancel({ reason }: { reason?: string } = {}) { 11 | cancelReason = reason; 12 | }, 13 | 14 | checkCancel(run: Run) { 15 | return cancelReason === undefined 16 | ? { shouldCancel: false as const } 17 | : { 18 | shouldCancel: true as const, 19 | reason: cancelReason, 20 | }; 21 | }, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/agent/src/agent/controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RunController"; 2 | export * from "./all"; 3 | export * from "./cancellable"; 4 | export * from "./maxSteps"; 5 | export * from "./noLimit"; 6 | -------------------------------------------------------------------------------- /packages/agent/src/agent/controller/maxSteps.ts: -------------------------------------------------------------------------------- 1 | import { Run } from ".."; 2 | import { RunController } from "./RunController"; 3 | 4 | export const maxSteps = ( 5 | maxSteps: number 6 | ): RunController => ({ 7 | checkCancel(run: Run) { 8 | if (run.root!.getStepCount() < maxSteps) { 9 | return { shouldCancel: false as const }; 10 | } 11 | 12 | return { 13 | shouldCancel: true as const, 14 | reason: `Maximum number of steps (${maxSteps}) exceeded.`, 15 | }; 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/agent/src/agent/controller/noLimit.ts: -------------------------------------------------------------------------------- 1 | import { Run } from ".."; 2 | import { RunController } from "./RunController"; 3 | 4 | export const noLimit = (): RunController => ({ 5 | checkCancel(run: Run) { 6 | return { shouldCancel: false as const }; 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/agent/src/agent/env/LoadEnvironmentKeyFunction.ts: -------------------------------------------------------------------------------- 1 | export type LoadEnvironmentKeyFunction = ( 2 | environmentKey: string 3 | ) => PromiseLike; 4 | -------------------------------------------------------------------------------- /packages/agent/src/agent/env/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./LoadEnvironmentKeyFunction"; 2 | export * from "./property"; 3 | -------------------------------------------------------------------------------- /packages/agent/src/agent/env/loadEnvironment.ts: -------------------------------------------------------------------------------- 1 | import { LoadEnvironmentKeyFunction } from "./LoadEnvironmentKeyFunction"; 2 | 3 | export async function loadEnvironment< 4 | ENVIRONMENT extends Record 5 | >( 6 | environment: Record 7 | ): Promise { 8 | const loadedEnvironment: Record = {}; 9 | 10 | for (const key of Object.keys(environment)) { 11 | const load = environment[key as keyof ENVIRONMENT]; 12 | loadedEnvironment[key] = await load(key); 13 | } 14 | 15 | return loadedEnvironment as ENVIRONMENT; 16 | } 17 | -------------------------------------------------------------------------------- /packages/agent/src/agent/env/property.ts: -------------------------------------------------------------------------------- 1 | export const property = 2 | (environmentKey: string) => async (): Promise => { 3 | const value = process.env[environmentKey]; 4 | 5 | if (!value) { 6 | throw new Error(`Environment property "${environmentKey}" is not set`); 7 | } 8 | 9 | return value; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/agent/src/agent/index.ts: -------------------------------------------------------------------------------- 1 | export * from "../server/ServerAgentSpecification"; 2 | export * from "./EmbedCall"; 3 | export * from "./GenerateCall"; 4 | export * from "./Run"; 5 | export * from "./RunContext"; 6 | export * from "./calculateRunCostInMillicent"; 7 | export * as controller from "./controller/index"; 8 | export * as env from "./env"; 9 | export * as observer from "./observer/index"; 10 | export * from "./runAgent"; 11 | -------------------------------------------------------------------------------- /packages/agent/src/agent/observer/RunObserver.ts: -------------------------------------------------------------------------------- 1 | import { StepResult } from "../../step"; 2 | import { Loop } from "../../step/Loop"; 3 | import { Step } from "../../step/Step"; 4 | import { Run } from "../Run"; 5 | 6 | export type RunObserver = { 7 | onRunStarted?: (_: { run: Run }) => void; 8 | onRunFinished?: (_: { run: Run; result: StepResult }) => void; 9 | 10 | onStepGenerationStarted?: (_: { run: Run }) => void; 11 | onStepGenerationFinished?: (_: { 12 | run: Run; 13 | generatedText: string; 14 | step: Step; 15 | }) => void; 16 | 17 | onLoopIterationStarted?: (_: { 18 | run: Run; 19 | loop: Loop; 20 | }) => void; 21 | onLoopIterationFinished?: (_: { 22 | run: Run; 23 | loop: Loop; 24 | }) => void; 25 | 26 | onStepExecutionStarted?: (_: { 27 | run: Run; 28 | step: Step; 29 | }) => void; 30 | onStepExecutionFinished?: (_: { 31 | run: Run; 32 | step: Step; 33 | result: StepResult; 34 | }) => void; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/agent/src/agent/observer/combineObservers.ts: -------------------------------------------------------------------------------- 1 | import { RunObserver } from "./RunObserver"; 2 | 3 | export const combineObservers = ( 4 | ...observers: RunObserver[] 5 | ): RunObserver => ({ 6 | onRunStarted: ({ run }) => { 7 | observers.forEach((observer) => observer.onRunStarted?.({ run })); 8 | }, 9 | 10 | onRunFinished: ({ run, result }) => { 11 | observers.forEach((observer) => observer.onRunFinished?.({ run, result })); 12 | }, 13 | 14 | onStepGenerationStarted: ({ run }) => { 15 | observers.forEach((observer) => 16 | observer.onStepGenerationStarted?.({ run }) 17 | ); 18 | }, 19 | 20 | onStepGenerationFinished: ({ run, generatedText, step }) => { 21 | observers.forEach((observer) => 22 | observer.onStepGenerationFinished?.({ run, generatedText, step }) 23 | ); 24 | }, 25 | 26 | onLoopIterationStarted: ({ run, loop }) => { 27 | observers.forEach((observer) => 28 | observer.onLoopIterationStarted?.({ run, loop }) 29 | ); 30 | }, 31 | 32 | onLoopIterationFinished: ({ run, loop }) => { 33 | observers.forEach((observer) => 34 | observer.onLoopIterationFinished?.({ run, loop }) 35 | ); 36 | }, 37 | 38 | onStepExecutionStarted: ({ run, step }) => { 39 | observers.forEach((observer) => 40 | observer.onStepExecutionStarted?.({ run, step }) 41 | ); 42 | }, 43 | 44 | onStepExecutionFinished: ({ run, step, result }) => { 45 | observers.forEach((observer) => 46 | observer.onStepExecutionFinished?.({ run, step, result }) 47 | ); 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /packages/agent/src/agent/observer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RunObserver"; 2 | export * from "./combineObservers"; 3 | export * from "./showRunInConsole"; 4 | -------------------------------------------------------------------------------- /packages/agent/src/agent/observer/showRunInConsole.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { Step, StepResult } from "../../step"; 3 | import { BasicToolStep } from "../../tool/BasicToolStep"; 4 | import { Run } from "../Run"; 5 | import { RunObserver } from "./RunObserver"; 6 | 7 | const log = console.log; 8 | 9 | export const showRunInConsole = ({ 10 | name, 11 | }: { 12 | name: string; 13 | }): RunObserver => ({ 14 | onRunStarted({ run }: { run: Run }) { 15 | log(chalk.green(`### ${name} ###`)); 16 | log(run.state); 17 | log(); 18 | }, 19 | 20 | onRunFinished({ result }: { result: StepResult }) { 21 | if (result.type === "cancelled") { 22 | log(chalk.gray(`Cancelled: ${result.reason}`)); 23 | return; 24 | } 25 | 26 | log(chalk.gray("Done")); 27 | log(result); 28 | }, 29 | 30 | onStepGenerationStarted() { 31 | log(chalk.gray("Thinking…")); 32 | }, 33 | 34 | onStepGenerationFinished({ generatedText }: { generatedText: string }) { 35 | log(chalk.cyanBright(generatedText)); 36 | log(); 37 | }, 38 | 39 | onStepExecutionStarted({ step }: { step: Step }) { 40 | if (step instanceof BasicToolStep) { 41 | log(chalk.gray(`Executing ${step.type}…`)); 42 | return; 43 | } 44 | }, 45 | 46 | onStepExecutionFinished({ step }: { step: Step }) { 47 | if (step instanceof BasicToolStep) { 48 | const result = step.state; 49 | const resultType = result.type; 50 | 51 | switch (resultType) { 52 | case "succeeded": { 53 | log(chalk.green(step.action.formatResult(result as any))); 54 | log(); 55 | break; 56 | } 57 | 58 | case "cancelled": { 59 | log(chalk.yellow("Cancelled")); 60 | log(); 61 | break; 62 | } 63 | 64 | case "failed": { 65 | log(chalk.red(`ERROR: ${result.error}`)); 66 | log(); 67 | break; 68 | } 69 | 70 | case "pending": 71 | case "running": { 72 | // ignored 73 | break; 74 | } 75 | 76 | default: { 77 | const _exhaustiveCheck: never = resultType; 78 | throw new Error(`Unhandled result type: ${resultType}`); 79 | } 80 | } 81 | return; 82 | } 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /packages/agent/src/agent/runAgent.ts: -------------------------------------------------------------------------------- 1 | import { StepFactory } from "../step/StepFactory"; 2 | import { maxSteps } from "./controller/maxSteps"; 3 | import { Run } from "./Run"; 4 | import { RunController } from "./controller/RunController"; 5 | import { RunObserver } from "./observer/RunObserver"; 6 | 7 | export const runAgent = async ({ 8 | agent, 9 | observer, 10 | controller = maxSteps(100), 11 | properties, 12 | }: { 13 | agent: StepFactory; 14 | controller?: RunController; 15 | observer: RunObserver; 16 | properties: RUN_STATE; 17 | }) => { 18 | const run = new Run({ 19 | controller, 20 | observer, 21 | initialState: properties, 22 | }); 23 | 24 | const rootStep = await agent(run); 25 | run.root = rootStep; 26 | 27 | run.onStart(); 28 | 29 | const result = await rootStep.execute(); 30 | 31 | run.onFinish({ result }); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/agent/src/convert/htmlToText.ts: -------------------------------------------------------------------------------- 1 | import { convert } from "html-to-text"; 2 | 3 | export const htmlToText = (content: string) => { 4 | const text = convert(content); 5 | return text.replace(/\[.*?\]/g, ""); // remove all links in square brackets 6 | }; 7 | 8 | htmlToText.asFunction = () => async (content: string) => htmlToText(content); 9 | -------------------------------------------------------------------------------- /packages/agent/src/convert/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./htmlToText"; 2 | export * from "./pdfToText"; 3 | -------------------------------------------------------------------------------- /packages/agent/src/convert/pdfToText.ts: -------------------------------------------------------------------------------- 1 | export async function pdfToText(data: ArrayBuffer) { 2 | // only load when needed (otherwise this can cause node canvas setup issues when you don't need PDFs): 3 | const PdfJs = await import("pdfjs-dist/legacy/build/pdf"); 4 | 5 | const pdf = await PdfJs.getDocument({ 6 | data, 7 | useSystemFonts: true, // https://github.com/mozilla/pdf.js/issues/4244#issuecomment-1479534301 8 | }).promise; 9 | 10 | const pageTexts: string[] = []; 11 | for (let i = 0; i < pdf.numPages; i++) { 12 | const page = await pdf.getPage(i + 1); 13 | const pageContent = await page.getTextContent(); 14 | 15 | pageTexts.push( 16 | pageContent.items 17 | // limit to TextItem, extract str: 18 | .filter((item) => (item as any).str != null) 19 | .map((item) => (item as any).str as string) 20 | .join(" ") 21 | ); 22 | } 23 | 24 | // reduce whitespace to single space 25 | return pageTexts.join("\n").replace(/\s+/g, " "); 26 | } 27 | 28 | pdfToText.asFunction = () => pdfToText; 29 | -------------------------------------------------------------------------------- /packages/agent/src/embedding/EmbedFunction.ts: -------------------------------------------------------------------------------- 1 | import { RunContext } from "../agent/RunContext"; 2 | 3 | export type EmbedFunction = ( 4 | options: { value: string }, 5 | context: RunContext 6 | ) => Promise; 7 | -------------------------------------------------------------------------------- /packages/agent/src/embedding/EmbeddingModel.ts: -------------------------------------------------------------------------------- 1 | export type EmbeddingModel = { 2 | vendor: string; 3 | name: string; 4 | embed: (value: string) => PromiseLike; 5 | extractEmbedding: (output: RAW_OUTPUT) => PromiseLike; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/agent/src/embedding/embed.ts: -------------------------------------------------------------------------------- 1 | import { RunContext } from "../agent/RunContext"; 2 | import { RetryFunction } from "../util/RetryFunction"; 3 | import { retryWithExponentialBackoff } from "../util/retryWithExponentialBackoff"; 4 | import { EmbeddingModel } from "./EmbeddingModel"; 5 | 6 | export async function embed( 7 | { 8 | id, 9 | model, 10 | value, 11 | retry = retryWithExponentialBackoff(), 12 | }: { 13 | id?: string; 14 | model: EmbeddingModel; 15 | value: string; 16 | retry?: RetryFunction; 17 | }, 18 | context: RunContext 19 | ) { 20 | const startTime = performance.now(); 21 | const startEpochSeconds = Math.floor( 22 | (performance.timeOrigin + startTime) / 1000 23 | ); 24 | 25 | const rawOutput = await retry(() => model.embed(value)); 26 | 27 | const textGenerationDurationInMs = Math.ceil(performance.now() - startTime); 28 | 29 | const metadata = { 30 | id, 31 | model: { 32 | vendor: model.vendor, 33 | name: model.name, 34 | }, 35 | startEpochSeconds, 36 | durationInMs: textGenerationDurationInMs, 37 | tries: rawOutput.tries, 38 | }; 39 | 40 | if (!rawOutput.success) { 41 | context?.recordCall?.({ 42 | type: "embed", 43 | success: false, 44 | metadata, 45 | input: value, 46 | error: rawOutput.error, 47 | }); 48 | 49 | throw rawOutput.error; // TODO wrap error 50 | } 51 | 52 | const embedding = await model.extractEmbedding(rawOutput.result); 53 | 54 | context?.recordCall?.({ 55 | type: "embed", 56 | success: true, 57 | metadata, 58 | input: value, 59 | rawOutput: rawOutput.result, 60 | embedding, 61 | }); 62 | 63 | return embedding; 64 | } 65 | 66 | embed.asFunction = 67 | ({ 68 | id, 69 | model, 70 | }: { 71 | id?: string; 72 | model: EmbeddingModel; 73 | }) => 74 | async ({ value }: { value: string }, context: RunContext) => 75 | embed({ id, model, value }, context); 76 | -------------------------------------------------------------------------------- /packages/agent/src/embedding/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./EmbeddingModel"; 2 | export * from "./embed"; 3 | -------------------------------------------------------------------------------- /packages/agent/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as action from "./action"; 2 | export * as agent from "./agent"; 3 | export * from "./agent/runAgent"; 4 | export * as convert from "./convert"; 5 | export * as embedding from "./embedding"; 6 | export * as prompt from "./prompt"; 7 | /** 8 | * API Providers (such as OpenAI). 9 | */ 10 | export * as provider from "./provider"; 11 | export * as server from "./server"; 12 | export * as source from "./source"; 13 | export * as step from "./step"; 14 | export * as text from "./text"; 15 | export * as textStore from "./text-store"; 16 | /** 17 | * Tokenize text. 18 | */ 19 | export * as tokenizer from "./tokenizer"; 20 | export * as tool from "./tool"; 21 | export * as util from "./util"; 22 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/FormatSectionFunction.ts: -------------------------------------------------------------------------------- 1 | import { Section } from "./Section"; 2 | 3 | export type FormatSectionFunction = (section: Section) => string; 4 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/Prompt.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIChatMessage } from "../provider/openai/api/OpenAIChatCompletion"; 2 | 3 | export type Prompt = ( 4 | input: INPUT 5 | ) => PromiseLike; 6 | 7 | export type ChatPrompt = Prompt>; 8 | 9 | export type TextPrompt = Prompt; 10 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/Section.ts: -------------------------------------------------------------------------------- 1 | export type Section = { 2 | title: string; 3 | content: string; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/availableActionsPrompt.ts: -------------------------------------------------------------------------------- 1 | import { ActionRegistry } from "../action"; 2 | import { chatPromptFromTextPrompt } from "./chatPromptFromTextPrompt"; 3 | import { sectionsTextPrompt } from "./sectionsPrompt"; 4 | 5 | export const availableActionsSections = async ({ 6 | actions, 7 | }: { 8 | actions: ActionRegistry; 9 | }) => [ 10 | { 11 | title: "Available Actions", 12 | content: actions.getAvailableActionInstructions(), 13 | }, 14 | ]; 15 | 16 | export const availableActionsTextPrompt = () => 17 | sectionsTextPrompt({ 18 | getSections: availableActionsSections, 19 | }); 20 | 21 | export const availableActionsChatPrompt = () => 22 | chatPromptFromTextPrompt({ 23 | role: "system", 24 | textPrompt: availableActionsTextPrompt(), 25 | }); 26 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/chatPromptFromTextPrompt.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIChatMessage } from "../provider/openai/api/OpenAIChatCompletion"; 2 | import { ChatPrompt, TextPrompt } from "./Prompt"; 3 | 4 | export const chatPromptFromTextPrompt = 5 | ({ 6 | textPrompt, 7 | role, 8 | }: { 9 | textPrompt: TextPrompt; 10 | role: OpenAIChatMessage["role"]; 11 | }): ChatPrompt => 12 | async (input: INPUT): Promise> => 13 | [{ role, content: await textPrompt(input) }]; 14 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/concatChatPrompts.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIChatMessage } from "../provider/openai/api/OpenAIChatCompletion"; 2 | import { ChatPrompt } from "./Prompt"; 3 | 4 | // Convenience function overloads for combining multiple chat prompts into one. 5 | // TypeScripts type inference (5.0) does not work well for merging multiple generics 6 | // if only the last overload is provided, so we provide a few overloads to make 7 | // it easier to use. 8 | export function concatChatPrompts( 9 | prompt1: ChatPrompt, 10 | prompt2: ChatPrompt 11 | ): ChatPrompt; 12 | export function concatChatPrompts( 13 | prompt1: ChatPrompt, 14 | prompt2: ChatPrompt, 15 | prompt3: ChatPrompt 16 | ): ChatPrompt; 17 | export function concatChatPrompts( 18 | prompt1: ChatPrompt, 19 | prompt2: ChatPrompt, 20 | prompt3: ChatPrompt, 21 | prompt4: ChatPrompt 22 | ): ChatPrompt; 23 | export function concatChatPrompts( 24 | prompt1: ChatPrompt, 25 | prompt2: ChatPrompt, 26 | prompt3: ChatPrompt, 27 | prompt4: ChatPrompt, 28 | prompt5: ChatPrompt 29 | ): ChatPrompt; 30 | export function concatChatPrompts(...prompts: Array>) { 31 | return async (input: INPUT): Promise> => { 32 | const messages: Array = []; 33 | for (const prompt of prompts) { 34 | const promptMessages = await prompt(input); 35 | messages.push(...promptMessages); 36 | } 37 | return messages; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/extractPrompt.ts: -------------------------------------------------------------------------------- 1 | export const extractAndExcludeChatPrompt = 2 | ({ excludeKeyword }: { excludeKeyword: string }) => 3 | async ({ text, topic }: { text: string; topic: string }) => 4 | [ 5 | { 6 | role: "user" as const, 7 | content: `## TOPIC\n${topic}`, 8 | }, 9 | { 10 | role: "system" as const, 11 | content: `## TASK 12 | Extract and write down information from the content below that is directly relevant for the topic above. 13 | Only include information that is directly relevant for the topic. 14 | Say "${excludeKeyword}" if there is no relevant information in the content.`, 15 | }, 16 | { 17 | role: "user" as const, 18 | content: `## CONTENT\n${text}`, 19 | }, 20 | ]; 21 | 22 | export const extractChatPrompt = 23 | () => 24 | async ({ text, topic }: { text: string; topic: string }) => 25 | [ 26 | { 27 | role: "user" as const, 28 | content: `## TOPIC\n${topic}`, 29 | }, 30 | { 31 | role: "system" as const, 32 | content: `## ROLE 33 | You are an expert at extracting information. 34 | You need to extract and keep all the information on the topic above topic from the text below. 35 | Only include information that is directly relevant for the topic.`, 36 | }, 37 | { 38 | role: "user" as const, 39 | content: `## TEXT\n${text}`, 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/formatSectionAsMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { FormatSectionFunction } from "./FormatSectionFunction"; 2 | import { Section } from "./Section"; 3 | 4 | export const formatSectionAsMarkdown: FormatSectionFunction = ( 5 | section: Section 6 | ): string => `## ${section.title.toUpperCase()}\n${section.content}`; 7 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./FormatSectionFunction"; 2 | export * from "./Prompt"; 3 | export * from "./Section"; 4 | export * from "./availableActionsPrompt"; 5 | export * from "./chatPromptFromTextPrompt"; 6 | export * from "./concatChatPrompts"; 7 | export * from "./extractPrompt"; 8 | export * from "./formatSectionAsMarkdown"; 9 | export * from "./recentStepsChatPrompt"; 10 | export * from "./rewritePrompt"; 11 | export * from "./sectionsPrompt"; 12 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/recentStepsChatPrompt.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIChatMessage } from "../provider/openai/api/OpenAIChatCompletion"; 2 | import { GenerateNextStepLoop, NoopStep, Step } from "../step"; 3 | import { BasicToolStep } from "../tool/BasicToolStep"; 4 | 5 | export const recentStepsChatPrompt = 6 | ({ maxSteps = 10 }: { maxSteps?: number }) => 7 | async ({ 8 | completedSteps, 9 | generatedTextsByStepId, 10 | }: { 11 | completedSteps: Array>; 12 | generatedTextsByStepId: Map; 13 | }): Promise => { 14 | const messages: OpenAIChatMessage[] = []; 15 | 16 | for (const step of completedSteps.slice(-maxSteps)) { 17 | // repeat the original agent response to reinforce the action format and keep the conversation going: 18 | const generatedText = generatedTextsByStepId.get(step.id); 19 | if (generatedText != null) { 20 | messages.push({ 21 | role: "assistant", 22 | content: generatedText, 23 | }); 24 | } 25 | 26 | let content: string | undefined = undefined; 27 | 28 | const stepState = step.state; 29 | switch (stepState.type) { 30 | case "failed": { 31 | content = `ERROR:\n${stepState.summary}`; 32 | break; 33 | } 34 | case "succeeded": { 35 | if (step instanceof NoopStep && step.isDoneStep()) { 36 | content = stepState.summary; 37 | } else if (step instanceof BasicToolStep) { 38 | content = step.action.formatResult({ 39 | input: stepState.input, 40 | output: stepState.output, 41 | summary: stepState.summary, 42 | }); 43 | } else if (step instanceof GenerateNextStepLoop) { 44 | content = stepState.summary; 45 | } 46 | break; 47 | } 48 | } 49 | 50 | if (content != null) { 51 | messages.push({ 52 | role: "system", 53 | content, 54 | }); 55 | } 56 | } 57 | 58 | return messages; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/rewritePrompt.ts: -------------------------------------------------------------------------------- 1 | export const rewriteChatPrompt = 2 | () => 3 | async ({ text, topic }: { text: string; topic: string }) => 4 | [ 5 | { 6 | role: "user" as const, 7 | content: `## TOPIC\n${topic}`, 8 | }, 9 | { 10 | role: "system" as const, 11 | content: `## TASK 12 | Rewrite the content below into a coherent text on the topic above. 13 | Include all relevant information about the topic. 14 | Discard all irrelevant information. 15 | The result can be as long as needed.`, 16 | }, 17 | { 18 | role: "user" as const, 19 | content: `## CONTENT\n${text}`, 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /packages/agent/src/prompt/sectionsPrompt.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIChatMessage } from "../provider/openai/api/OpenAIChatCompletion"; 2 | import { chatPromptFromTextPrompt } from "./chatPromptFromTextPrompt"; 3 | import { formatSectionAsMarkdown } from "./formatSectionAsMarkdown"; 4 | import { FormatSectionFunction } from "./FormatSectionFunction"; 5 | import { ChatPrompt, TextPrompt } from "./Prompt"; 6 | import { Section } from "./Section"; 7 | 8 | export const sectionsTextPrompt = 9 | ({ 10 | sectionSeparator = "\n\n", 11 | format = formatSectionAsMarkdown, 12 | getSections, 13 | }: { 14 | sectionSeparator?: string; 15 | format?: FormatSectionFunction; 16 | getSections: (input: INPUT) => PromiseLike>; 17 | }): TextPrompt => 18 | async (input: INPUT) => 19 | (await getSections(input)).map(format).join(sectionSeparator); 20 | 21 | export const sectionsChatPrompt = ({ 22 | role, 23 | sectionSeparator, 24 | format, 25 | getSections, 26 | }: { 27 | role: OpenAIChatMessage["role"]; 28 | sectionSeparator?: string; 29 | format?: FormatSectionFunction; 30 | getSections: (input: INPUT) => PromiseLike>; 31 | }): ChatPrompt => 32 | chatPromptFromTextPrompt({ 33 | role, 34 | textPrompt: sectionsTextPrompt({ 35 | sectionSeparator, 36 | format, 37 | getSections, 38 | }), 39 | }); 40 | -------------------------------------------------------------------------------- /packages/agent/src/provider/index.ts: -------------------------------------------------------------------------------- 1 | export * as openai from "./openai/index.js"; 2 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/api/OpenAIChatCompletion.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | 3 | export const OpenAIChatCompletionSchema = zod.object({ 4 | id: zod.string(), 5 | object: zod.literal("chat.completion"), 6 | created: zod.number(), 7 | model: zod.string(), 8 | choices: zod.array( 9 | zod.object({ 10 | message: zod.object({ 11 | role: zod.literal("assistant"), 12 | content: zod.string(), 13 | }), 14 | index: zod.number(), 15 | logprobs: zod.nullable(zod.any()), 16 | finish_reason: zod.string(), 17 | }) 18 | ), 19 | usage: zod.object({ 20 | prompt_tokens: zod.number(), 21 | completion_tokens: zod.number(), 22 | total_tokens: zod.number(), 23 | }), 24 | }); 25 | 26 | export type OpenAIChatCompletion = zod.infer; 27 | 28 | export type OpenAIChatMessage = { 29 | role: "user" | "assistant" | "system"; 30 | content: string; 31 | }; 32 | 33 | export type OpenAIChatCompletionModel = "gpt-4" | "gpt-3.5-turbo"; 34 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/api/OpenAIEmbedding.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | 3 | export const OpenAIEmbeddingSchema = zod.object({ 4 | object: zod.literal("list"), 5 | data: zod 6 | .array( 7 | zod.object({ 8 | object: zod.literal("embedding"), 9 | embedding: zod.array(zod.number()), 10 | index: zod.number(), 11 | }) 12 | ) 13 | .length(1), 14 | model: zod.string(), 15 | usage: zod.object({ 16 | prompt_tokens: zod.number(), 17 | total_tokens: zod.number(), 18 | }), 19 | }); 20 | 21 | export type OpenAIEmbedding = zod.infer; 22 | 23 | export type OpenAIEmbeddingModel = "text-embedding-ada-002"; 24 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/api/OpenAIError.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import zod from "zod"; 3 | 4 | export const OpenAIErrorDataSchema = zod.object({ 5 | error: zod.object({ 6 | message: zod.string(), 7 | type: zod.string(), 8 | param: zod.any().nullable(), 9 | code: zod.string(), 10 | }), 11 | }); 12 | 13 | export type OpenAIErrorData = zod.infer; 14 | 15 | export class OpenAIError extends Error { 16 | public readonly code: OpenAIErrorData["error"]["code"]; 17 | public readonly type: OpenAIErrorData["error"]["type"]; 18 | 19 | constructor({ error: { message, code, type } }: OpenAIErrorData) { 20 | super(message); 21 | this.code = code; 22 | this.type = type; 23 | } 24 | } 25 | 26 | export const withOpenAIErrorHandler = async (fn: () => PromiseLike) => { 27 | try { 28 | return await fn(); 29 | } catch (error) { 30 | if (error instanceof AxiosError) { 31 | const parsedError = OpenAIErrorDataSchema.safeParse( 32 | error?.response?.data 33 | ); 34 | 35 | if (parsedError.success) { 36 | throw new OpenAIError(parsedError.data); 37 | } 38 | } 39 | 40 | throw error; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/api/OpenAITextCompletion.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | 3 | export const OpenAITextCompletionSchema = zod.object({ 4 | id: zod.string(), 5 | object: zod.literal("text_completion"), 6 | created: zod.number(), 7 | model: zod.string(), 8 | choices: zod.array( 9 | zod.object({ 10 | text: zod.string(), 11 | index: zod.number(), 12 | logprobs: zod.nullable(zod.any()), 13 | finish_reason: zod.string(), 14 | }) 15 | ), 16 | usage: zod.object({ 17 | prompt_tokens: zod.number(), 18 | completion_tokens: zod.number(), 19 | total_tokens: zod.number(), 20 | }), 21 | }); 22 | 23 | export type OpenAITextCompletion = zod.infer; 24 | 25 | export type OpenAITextCompletionModel = 26 | | "text-davinci-003" 27 | | "text-davinci-002" 28 | | "code-davinci-002" 29 | | "code-davinci-002" 30 | | "text-curie-001" 31 | | "text-babbage-001" 32 | | "text-ada-001" 33 | | "davinci" 34 | | "curie" 35 | | "babbage" 36 | | "ada"; 37 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/api/generateChatCompletion.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | OpenAIChatCompletion, 4 | OpenAIChatCompletionModel, 5 | OpenAIChatCompletionSchema, 6 | OpenAIChatMessage, 7 | } from "./OpenAIChatCompletion"; 8 | import { withOpenAIErrorHandler } from "./OpenAIError"; 9 | 10 | export async function generateChatCompletion({ 11 | baseUrl = "https://api.openai.com/v1", 12 | apiKey, 13 | model, 14 | messages, 15 | n, 16 | temperature, 17 | maxTokens, 18 | presencePenalty, 19 | frequencyPenalty, 20 | }: { 21 | baseUrl?: string; 22 | apiKey: string; 23 | messages: Array; 24 | model: OpenAIChatCompletionModel; 25 | n?: number; 26 | temperature?: number; 27 | maxTokens?: number; 28 | presencePenalty?: number; 29 | frequencyPenalty?: number; 30 | }): Promise { 31 | return withOpenAIErrorHandler(async () => { 32 | const response = await axios.post( 33 | `${baseUrl}/chat/completions`, 34 | JSON.stringify({ 35 | model, 36 | messages, 37 | n, 38 | temperature, 39 | max_tokens: maxTokens, 40 | presence_penalty: presencePenalty, 41 | frequency_penalty: frequencyPenalty, 42 | }), 43 | { 44 | headers: { 45 | "Content-Type": "application/json", 46 | Authorization: `Bearer ${apiKey}`, 47 | }, 48 | } 49 | ); 50 | 51 | return OpenAIChatCompletionSchema.parse(response.data); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/api/generateEmbedding.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | OpenAIEmbedding, 4 | OpenAIEmbeddingModel, 5 | OpenAIEmbeddingSchema, 6 | } from "./OpenAIEmbedding"; 7 | 8 | export async function generateEmbedding({ 9 | baseUrl = "https://api.openai.com/v1", 10 | apiKey, 11 | model, 12 | input, 13 | }: { 14 | baseUrl?: string; 15 | apiKey: string; 16 | model: OpenAIEmbeddingModel; 17 | input: string; 18 | }): Promise { 19 | const response = await axios.post( 20 | `${baseUrl}/embeddings`, 21 | JSON.stringify({ 22 | model, 23 | input, 24 | }), 25 | { 26 | headers: { 27 | "Content-Type": "application/json", 28 | Authorization: `Bearer ${apiKey}`, 29 | }, 30 | } 31 | ); 32 | 33 | return OpenAIEmbeddingSchema.parse(response.data); 34 | } 35 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/api/generateTextCompletion.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { withOpenAIErrorHandler } from "./OpenAIError"; 3 | import { 4 | OpenAITextCompletion, 5 | OpenAITextCompletionModel, 6 | OpenAITextCompletionSchema, 7 | } from "./OpenAITextCompletion"; 8 | 9 | export async function generateTextCompletion({ 10 | baseUrl = "https://api.openai.com/v1", 11 | apiKey, 12 | prompt, 13 | model, 14 | n, 15 | temperature, 16 | maxTokens, 17 | presencePenalty, 18 | frequencyPenalty, 19 | }: { 20 | baseUrl?: string; 21 | apiKey: string; 22 | prompt: string; 23 | model: OpenAITextCompletionModel; 24 | n?: number; 25 | temperature?: number; 26 | maxTokens?: number; 27 | presencePenalty?: number; 28 | frequencyPenalty?: number; 29 | }): Promise { 30 | return withOpenAIErrorHandler(async () => { 31 | const response = await axios.post( 32 | `${baseUrl}/completions`, 33 | JSON.stringify({ 34 | model, 35 | prompt, 36 | n, 37 | temperature, 38 | max_tokens: maxTokens, 39 | presence_penalty: presencePenalty, 40 | frequency_penalty: frequencyPenalty, 41 | }), 42 | { 43 | headers: { 44 | "Content-Type": "application/json", 45 | Authorization: `Bearer ${apiKey}`, 46 | }, 47 | } 48 | ); 49 | 50 | return OpenAITextCompletionSchema.parse(response.data); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./OpenAIChatCompletion.js"; 2 | export * from "./OpenAIEmbedding.js"; 3 | export * from "./OpenAIError.js"; 4 | export * from "./OpenAITextCompletion.js"; 5 | export * from "./generateChatCompletion.js"; 6 | export * from "./generateEmbedding.js"; 7 | export * from "./generateTextCompletion.js"; 8 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/chatModel.ts: -------------------------------------------------------------------------------- 1 | import { GeneratorModel } from "../../text/generate/GeneratorModel"; 2 | import { 3 | OpenAIChatCompletion, 4 | OpenAIChatCompletionModel, 5 | OpenAIChatMessage, 6 | } from "./api/OpenAIChatCompletion"; 7 | import { generateChatCompletion } from "./api/generateChatCompletion"; 8 | 9 | export const chatModel = ({ 10 | baseUrl, 11 | apiKey, 12 | model, 13 | temperature = 0, 14 | maxTokens, 15 | }: { 16 | baseUrl?: string; 17 | apiKey: string; 18 | model: OpenAIChatCompletionModel; 19 | temperature?: number; 20 | maxTokens?: number; 21 | }): GeneratorModel => ({ 22 | vendor: "openai", 23 | name: model, 24 | generate: async (input: OpenAIChatMessage[]): Promise => 25 | generateChatCompletion({ 26 | baseUrl, 27 | apiKey, 28 | messages: input, 29 | model, 30 | temperature, 31 | maxTokens, 32 | }), 33 | extractOutput: async (rawOutput: OpenAIChatCompletion): Promise => { 34 | return rawOutput.choices[0]!.message.content; 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/cost/calculateChatCompletionCostInMillicent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OpenAIChatCompletion, 3 | OpenAIChatCompletionModel, 4 | } from "../api/OpenAIChatCompletion"; 5 | 6 | // see https://openai.com/pricing 7 | const promptTokenCostInMillicent = { 8 | "gpt-4": 3, 9 | "gpt-3.5-turbo": 0.2, 10 | }; 11 | 12 | const completionTokenCostInMillicent = { 13 | "gpt-4": 6, 14 | "gpt-3.5-turbo": 0.2, 15 | }; 16 | 17 | export const calculateChatCompletionCostInMillicent = ({ 18 | model, 19 | output, 20 | }: { 21 | model: OpenAIChatCompletionModel; 22 | output: OpenAIChatCompletion; 23 | }) => 24 | output.usage.prompt_tokens * promptTokenCostInMillicent[model] + 25 | output.usage.completion_tokens * completionTokenCostInMillicent[model]; 26 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/cost/calculateEmbeddingCostInMillicent.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIEmbedding, OpenAIEmbeddingModel } from "../api/OpenAIEmbedding"; 2 | 3 | // see https://openai.com/pricing 4 | const tokenCostInMillicent = { 5 | "text-embedding-ada-002": 0.04, 6 | }; 7 | 8 | export const calculateEmbeddingCostInMillicent = ({ 9 | model, 10 | output, 11 | }: { 12 | model: OpenAIEmbeddingModel; 13 | output: OpenAIEmbedding; 14 | }) => tokenCostInMillicent[model] * output.usage.total_tokens; 15 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/cost/calculateOpenAICallCostInMillicent.ts: -------------------------------------------------------------------------------- 1 | import { EmbedCall } from "../../../agent/EmbedCall"; 2 | import { GenerateCall } from "../../../agent/GenerateCall"; 3 | import { OpenAIChatCompletionSchema } from "../api/OpenAIChatCompletion"; 4 | import { OpenAIEmbeddingSchema } from "../api/OpenAIEmbedding"; 5 | import { OpenAITextCompletionSchema } from "../api/OpenAITextCompletion"; 6 | import { calculateChatCompletionCostInMillicent } from "./calculateChatCompletionCostInMillicent"; 7 | import { calculateEmbeddingCostInMillicent } from "./calculateEmbeddingCostInMillicent"; 8 | import { calculateTextCompletionCostInMillicent } from "./calculateTextCompletionCostInMillicent"; 9 | 10 | export function calculateOpenAICallCostInMillicent( 11 | call: (GenerateCall | EmbedCall) & { 12 | success: true; 13 | } 14 | ) { 15 | if (call.metadata.model.vendor !== "openai") { 16 | throw new Error(`Incorrect vendor: ${call.metadata.model.vendor}`); 17 | } 18 | 19 | const type = call.type; 20 | 21 | switch (type) { 22 | case "generate": { 23 | const model = call.metadata.model.name; 24 | 25 | switch (model) { 26 | case "gpt-3.5-turbo": 27 | case "gpt-4": { 28 | return calculateChatCompletionCostInMillicent({ 29 | model, 30 | output: OpenAIChatCompletionSchema.parse(call.rawOutput), 31 | }); 32 | } 33 | 34 | case "text-davinci-003": 35 | case "text-davinci-002": 36 | case "code-davinci-002": 37 | case "text-curie-001": 38 | case "text-babbage-001": 39 | case "text-ada-001": 40 | case "davinci": 41 | case "curie": 42 | case "babbage": 43 | case "ada": { 44 | return calculateTextCompletionCostInMillicent({ 45 | model, 46 | output: OpenAITextCompletionSchema.parse(call.rawOutput), 47 | }); 48 | } 49 | 50 | default: { 51 | throw new Error(`Unknown model: ${model}`); 52 | } 53 | } 54 | } 55 | 56 | case "embed": { 57 | const model = call.metadata.model.name; 58 | 59 | switch (model) { 60 | case "text-embedding-ada-002": { 61 | return calculateEmbeddingCostInMillicent({ 62 | model, 63 | output: OpenAIEmbeddingSchema.parse(call.rawOutput), 64 | }); 65 | } 66 | 67 | default: { 68 | throw new Error(`Unknown model: ${model}`); 69 | } 70 | } 71 | } 72 | 73 | default: { 74 | const _exhaustiveCheck: never = type; 75 | throw new Error(`Unknown type: ${_exhaustiveCheck}`); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/cost/calculateTextCompletionCostInMillicent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OpenAITextCompletion, 3 | OpenAITextCompletionModel, 4 | } from "../api/OpenAITextCompletion"; 5 | 6 | // see https://openai.com/pricing 7 | const tokenCostInMillicent = { 8 | davinci: 2, 9 | "text-davinci-003": 2, 10 | "text-davinci-002": 2, 11 | curie: 0.2, 12 | "text-curie-001": 0.2, 13 | babbage: 0.05, 14 | "text-babbage-001": 0.05, 15 | ada: 0.04, 16 | "text-ada-001": 0.04, 17 | "code-davinci-002": 0, 18 | }; 19 | 20 | export const calculateTextCompletionCostInMillicent = ({ 21 | model, 22 | output, 23 | }: { 24 | model: OpenAITextCompletionModel; 25 | output: OpenAITextCompletion; 26 | }) => tokenCostInMillicent[model] * output.usage.total_tokens; 27 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/cost/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./calculateChatCompletionCostInMillicent.js"; 2 | export * from "./calculateEmbeddingCostInMillicent.js"; 3 | export * from "./calculateTextCompletionCostInMillicent.js"; 4 | export * from "./calculateOpenAICallCostInMillicent.js"; 5 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/embeddingModel.ts: -------------------------------------------------------------------------------- 1 | import { EmbeddingModel } from "../../embedding/EmbeddingModel"; 2 | import { OpenAIEmbedding, OpenAIEmbeddingModel } from "./api/OpenAIEmbedding"; 3 | import { generateEmbedding } from "./api/generateEmbedding"; 4 | 5 | export const embeddingModel = ({ 6 | baseUrl, 7 | apiKey, 8 | model = "text-embedding-ada-002", 9 | }: { 10 | baseUrl?: string; 11 | apiKey: string; 12 | model?: OpenAIEmbeddingModel; 13 | }): EmbeddingModel => ({ 14 | vendor: "openai", 15 | name: model, 16 | embed: async (input: string): Promise => 17 | generateEmbedding({ 18 | baseUrl, 19 | apiKey, 20 | input, 21 | model, 22 | }), 23 | extractEmbedding: async (rawOutput: OpenAIEmbedding): Promise => 24 | rawOutput.data[0]!.embedding, 25 | }); 26 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Functions from the [OpenAI API](https://platform.openai.com/docs/api-reference). 3 | */ 4 | export * as api from "./api/index.js"; 5 | 6 | /** 7 | * Cost calculations based on the [OpenAI pricing](https://openai.com/pricing). 8 | */ 9 | export * as cost from "./cost/index.js"; 10 | 11 | /** 12 | * Tokenizer for OpenAI models (using [`@bqbd/tiktoken`](https://github.com/dqbd/tiktoken/tree/main/js)). 13 | */ 14 | export * as tokenizer from "./tokenizer.js"; 15 | 16 | // models 17 | export * from "./chatModel.js"; 18 | export * from "./embeddingModel.js"; 19 | export * from "./textModel.js"; 20 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/textModel.ts: -------------------------------------------------------------------------------- 1 | import { GeneratorModel } from "../../text/generate/GeneratorModel"; 2 | import { 3 | OpenAITextCompletion, 4 | OpenAITextCompletionModel, 5 | } from "./api/OpenAITextCompletion"; 6 | import { generateTextCompletion } from "./api/generateTextCompletion"; 7 | 8 | export const textModel = ({ 9 | baseUrl, 10 | apiKey, 11 | model, 12 | temperature = 0, 13 | maxTokens, 14 | }: { 15 | baseUrl?: string; 16 | apiKey: string; 17 | model: OpenAITextCompletionModel; 18 | temperature?: number; 19 | maxTokens?: number; 20 | }): GeneratorModel => ({ 21 | vendor: "openai", 22 | name: model, 23 | generate: async (input: string): Promise => 24 | generateTextCompletion({ 25 | baseUrl, 26 | apiKey, 27 | prompt: input, 28 | model, 29 | temperature, 30 | maxTokens, 31 | }), 32 | extractOutput: async (rawOutput: OpenAITextCompletion): Promise => { 33 | return rawOutput.choices[0]!.text; 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /packages/agent/src/provider/openai/tokenizer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | get_encoding, 3 | encoding_for_model, 4 | TiktokenEncoding, 5 | Tiktoken, 6 | TiktokenModel, 7 | } from "@dqbd/tiktoken"; 8 | import { Tokenizer } from "../../tokenizer/Tokenizer"; 9 | 10 | export function forModel({ model }: { model: TiktokenModel }): Tokenizer { 11 | return forTiktokenEncoder({ 12 | encoder: () => encoding_for_model(model), 13 | }); 14 | } 15 | 16 | export function forEncoding({ 17 | encoding, 18 | }: { 19 | encoding: TiktokenEncoding; 20 | }): Tokenizer { 21 | return forTiktokenEncoder({ encoder: () => get_encoding(encoding) }); 22 | } 23 | 24 | export function forTiktokenEncoder({ 25 | encoder: createEncoder, 26 | }: { 27 | encoder: () => Tiktoken; 28 | }): Tokenizer { 29 | const textDecoder = new TextDecoder(); 30 | return { 31 | encode: async (text: string) => { 32 | const encoder = createEncoder(); 33 | 34 | try { 35 | const tokens = encoder.encode(text); 36 | 37 | const tokenTexts: Array = []; 38 | for (const token of tokens) { 39 | tokenTexts.push( 40 | textDecoder.decode(encoder.decode_single_token_bytes(token)) 41 | ); 42 | } 43 | 44 | return { 45 | tokens, 46 | texts: tokenTexts, 47 | }; 48 | } finally { 49 | encoder.free(); 50 | } 51 | }, 52 | 53 | decode: async (tokens: Uint32Array) => { 54 | const encoder = createEncoder(); 55 | try { 56 | return textDecoder.decode(encoder.decode(tokens)); 57 | } finally { 58 | encoder.free(); 59 | } 60 | }, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /packages/agent/src/server/AgentPlugin.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from "fastify"; 2 | import { ZodTypeProvider } from "fastify-type-provider-zod"; 3 | import zod from "zod"; 4 | import { ServerAgent } from "./ServerAgent"; 5 | import { ServerAgentSpecification } from "./ServerAgentSpecification"; 6 | 7 | export const AgentPlugin = < 8 | ENVIRONMENT extends Record, 9 | INPUT, 10 | RUN_STATE extends INPUT, 11 | DATA 12 | >({ 13 | name, 14 | specification, 15 | }: { 16 | name: string; 17 | specification: ServerAgentSpecification; 18 | }) => 19 | async function plugin(server: FastifyInstance) { 20 | const serverAgent = await ServerAgent.create({ 21 | specification, 22 | }); 23 | 24 | const typedServer = server.withTypeProvider(); 25 | 26 | // create agent run (POST /agent/:agent) 27 | typedServer.route({ 28 | method: "POST", 29 | url: `/agent/${name}`, 30 | schema: { 31 | body: specification.inputSchema, 32 | }, 33 | async handler(request, reply) { 34 | const runId = await serverAgent.createRun({ 35 | input: request.body as INPUT, 36 | }); 37 | 38 | reply.code(201).send({ runId }); 39 | }, 40 | }); 41 | 42 | // start agent run (POST /agent/:agent/run/:runId/start) 43 | typedServer.route({ 44 | method: "POST", 45 | url: `/agent/${name}/run/:runId/start`, 46 | schema: { 47 | params: zod.object({ 48 | runId: zod.string(), 49 | }), 50 | }, 51 | async handler(request, reply) { 52 | const runId = request.params.runId; 53 | serverAgent.startRunWithoutWaiting({ runId }); 54 | reply.code(201).send({ runId }); 55 | }, 56 | }); 57 | 58 | // cancel agent run (POST /agent/:agent/run/:runId/cancel) 59 | typedServer.route({ 60 | method: "POST", 61 | url: `/agent/${name}/run/:runId/cancel`, 62 | schema: { 63 | params: zod.object({ 64 | runId: zod.string(), 65 | }), 66 | body: zod.object({ 67 | reason: zod.string().optional(), 68 | }), 69 | }, 70 | async handler(request, reply) { 71 | serverAgent.cancelRun({ 72 | runId: request.params.runId, 73 | reason: request.body.reason, 74 | }); 75 | 76 | reply.code(201).send({ runId: request.params.runId }); 77 | }, 78 | }); 79 | 80 | // get agent run status: 81 | typedServer.route({ 82 | method: "GET", 83 | url: `/agent/${name}/run/:runId`, 84 | schema: { 85 | params: zod.object({ 86 | runId: zod.string(), 87 | }), 88 | }, 89 | async handler(request, reply) { 90 | const runId = request.params.runId; 91 | const state = await serverAgent.getRunState({ runId }); 92 | 93 | const accept = request.accepts(); 94 | switch (accept.type(["json", "html"])) { 95 | case "json": { 96 | reply.header("Content-Type", "application/json"); 97 | reply.send({ state }); 98 | break; 99 | } 100 | case "html": { 101 | reply.header("Content-Type", "text/html"); 102 | reply.send(` 103 | 104 | 105 | Run ${runId} 106 | 107 | 108 |
${JSON.stringify(state, null, 2)}
109 | 110 | 111 | `); 112 | break; 113 | } 114 | default: { 115 | reply.code(406).send({ error: "Not Acceptable" }); 116 | break; 117 | } 118 | } 119 | }, 120 | }); 121 | }; 122 | -------------------------------------------------------------------------------- /packages/agent/src/server/ServerAgentSpecification.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | import { StepFactory } from "../step/StepFactory"; 3 | import { RunController } from "../agent/controller/RunController"; 4 | import { LoadEnvironmentKeyFunction } from "../agent/env/LoadEnvironmentKeyFunction"; 5 | import { RunObserver } from "../agent/observer"; 6 | import { Run } from "../agent"; 7 | 8 | export type DataProvider = RunObserver & { 9 | getData({ run }: { run: Run }): Promise; 10 | }; 11 | 12 | export type ServerAgentSpecification< 13 | ENVIRONMENT extends Record, 14 | INPUT, 15 | RUN_STATE extends INPUT, 16 | DATA 17 | > = { 18 | environment: Record; 19 | inputSchema: zod.ZodSchema; 20 | init: (options: { 21 | input: INPUT; 22 | environment: ENVIRONMENT; 23 | }) => Promise; 24 | execute: (options: { 25 | environment: ENVIRONMENT; 26 | }) => Promise>; 27 | controller?: RunController; 28 | createDataProvider: () => DataProvider; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/agent/src/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ServerAgentSpecification"; 2 | -------------------------------------------------------------------------------- /packages/agent/src/source/fileAsArrayBuffer.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | 3 | export const fileAsArrayBuffer = async ({ 4 | path, 5 | }: { 6 | path: string; 7 | }): Promise => { 8 | const rawBuffer = await fs.readFile(path); 9 | 10 | return rawBuffer.buffer.slice( 11 | rawBuffer.byteOffset, 12 | rawBuffer.byteOffset + rawBuffer.byteLength 13 | ); 14 | }; 15 | 16 | fileAsArrayBuffer.asFunction = 17 | () => 18 | async ({ path }: { path: string }) => 19 | fileAsArrayBuffer({ path }); 20 | -------------------------------------------------------------------------------- /packages/agent/src/source/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fileAsArrayBuffer"; 2 | export * from "./webpageAsHtmlText"; 3 | -------------------------------------------------------------------------------- /packages/agent/src/source/webpageAsHtmlText.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const webpageAsHtmlText = async ({ 4 | url, 5 | }: { 6 | url: string; 7 | }): Promise => { 8 | const result = await axios.get(url); 9 | return result.data; 10 | }; 11 | 12 | webpageAsHtmlText.asFunction = 13 | () => 14 | async ({ url }: { url: string }) => 15 | webpageAsHtmlText({ url }); 16 | -------------------------------------------------------------------------------- /packages/agent/src/step/ErrorStep.ts: -------------------------------------------------------------------------------- 1 | import { Run } from "../agent"; 2 | import { Step } from "./Step"; 3 | import { StepResult } from "./StepResult"; 4 | 5 | export class ErrorStep extends Step { 6 | readonly errorMessage: string | undefined; 7 | readonly error: unknown; 8 | 9 | constructor({ 10 | type = "error", 11 | run, 12 | errorMessage, 13 | error, 14 | }: { 15 | type?: string; 16 | run: Run; 17 | errorMessage?: string; 18 | error: unknown; 19 | }) { 20 | super({ type, run }); 21 | 22 | this.errorMessage = errorMessage; 23 | this.error = error; 24 | } 25 | 26 | protected async _execute(): Promise { 27 | return { 28 | type: "failed", 29 | summary: 30 | this.errorMessage ?? (this.error as any)?.message ?? "Task failed.", 31 | error: this.error, 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/agent/src/step/FixedStepsLoop.ts: -------------------------------------------------------------------------------- 1 | import { Run } from "../agent"; 2 | import { Loop } from "./Loop"; 3 | import { Step } from "./Step"; 4 | import { StepFactory } from "./StepFactory"; 5 | 6 | export const createFixedStepsLoop = 7 | ({ 8 | type, 9 | steps: stepFactories, 10 | }: { 11 | type?: string; 12 | steps: Array>; 13 | }): StepFactory => 14 | async (run) => { 15 | const steps = []; 16 | for (const factory of stepFactories) { 17 | steps.push(await factory(run)); 18 | } 19 | 20 | return new FixedStepsLoop({ type, run, steps }); 21 | }; 22 | 23 | export class FixedStepsLoop extends Loop { 24 | readonly steps: Array> = []; 25 | currentStep = 0; 26 | 27 | constructor({ 28 | type = "loop.fixed-steps", 29 | run, 30 | steps, 31 | }: { 32 | type?: string; 33 | run: Run; 34 | steps: Array>; 35 | }) { 36 | super({ type, run }); 37 | this.steps = steps; 38 | } 39 | 40 | protected async getNextStep() { 41 | return this.steps[this.currentStep++]!; 42 | } 43 | 44 | protected hasMoreSteps(): boolean { 45 | return this.currentStep < this.steps.length; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/agent/src/step/GenerateChatCompletionFunction.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIChatMessage } from "../provider/openai/api/OpenAIChatCompletion"; 2 | 3 | export type GenerateChatCompletionFunction = (parameters: { 4 | messages: Array; 5 | }) => PromiseLike; 6 | -------------------------------------------------------------------------------- /packages/agent/src/step/Loop.ts: -------------------------------------------------------------------------------- 1 | import { Run } from "../agent/Run"; 2 | import { Step } from "./Step"; 3 | import { StepResult } from "./StepResult"; 4 | 5 | export abstract class Loop extends Step { 6 | readonly completedSteps: Array> = []; 7 | 8 | constructor({ type, run }: { type: string; run: Run }) { 9 | super({ type, run }); 10 | } 11 | 12 | protected abstract hasMoreSteps(): boolean; 13 | 14 | protected abstract getNextStep(): PromiseLike>; 15 | 16 | protected async update({ 17 | step, 18 | result, 19 | }: { 20 | step: Step; 21 | result: StepResult & { 22 | type: "succeeded" | "failed"; 23 | }; 24 | }): Promise { 25 | return Promise.resolve(); 26 | } 27 | 28 | protected async getResult(): Promise { 29 | return { type: "succeeded", summary: "Completed all tasks." }; 30 | } 31 | 32 | protected async _execute(): Promise { 33 | try { 34 | while (this.hasMoreSteps()) { 35 | const cancelCheck = this.run.checkCancel(); 36 | if (cancelCheck.shouldCancel) { 37 | return { type: "cancelled", reason: cancelCheck.reason }; 38 | } 39 | 40 | this.run.onLoopIterationStarted({ loop: this }); 41 | 42 | const step = await this.getNextStep(); 43 | const result = await step.execute(); 44 | 45 | this.completedSteps.push(step); 46 | 47 | if (result.type === "cancelled") { 48 | return result; 49 | } 50 | 51 | await this.update({ step, result }); 52 | 53 | this.run.onLoopIterationFinished({ loop: this }); 54 | } 55 | } catch (error) { 56 | return { 57 | type: "failed", 58 | summary: `Failed to run step`, // TODO better summary 59 | error, 60 | }; 61 | } 62 | 63 | return this.getResult(); 64 | } 65 | 66 | getStepCount(): number { 67 | return this.completedSteps.reduce( 68 | (sum, step) => sum + step.getStepCount(), 69 | 0 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/agent/src/step/NoopStep.ts: -------------------------------------------------------------------------------- 1 | import { Run } from "../agent"; 2 | import { Step } from "./Step"; 3 | import { StepResult } from "./StepResult"; 4 | 5 | export class NoopStep extends Step { 6 | readonly summary: string; 7 | private readonly _isDoneStep: boolean; 8 | 9 | constructor({ 10 | type = "noop", 11 | run, 12 | summary, 13 | isDoneStep = false, 14 | }: { 15 | type?: string; 16 | run: Run; 17 | summary: string; 18 | isDoneStep?: boolean; 19 | }) { 20 | super({ type, run }); 21 | this.summary = summary; 22 | this._isDoneStep = isDoneStep; 23 | } 24 | 25 | protected async _execute(): Promise { 26 | return { type: "succeeded", summary: this.summary }; 27 | } 28 | 29 | isDoneStep() { 30 | return this._isDoneStep; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/agent/src/step/PromptStep.ts: -------------------------------------------------------------------------------- 1 | import { Run } from "../agent"; 2 | import { RunContext } from "../agent/RunContext"; 3 | import { Prompt } from "../prompt/Prompt"; 4 | import { generateText } from "../text/generate/generateText"; 5 | import { GeneratorModel } from "../text/generate/GeneratorModel"; 6 | import { Step } from "./Step"; 7 | import { StepResult } from "./StepResult"; 8 | 9 | export class PromptStep extends Step { 10 | private readonly generateText: ( 11 | _0: INPUT, 12 | _1: RunContext 13 | ) => PromiseLike; 14 | private readonly input: INPUT; 15 | 16 | constructor({ 17 | type = "prompt", 18 | run, 19 | prompt, 20 | model, 21 | input, 22 | }: { 23 | type?: string; 24 | run: Run; 25 | prompt: Prompt; 26 | model: GeneratorModel; 27 | input: INPUT; 28 | }) { 29 | super({ type, run }); 30 | 31 | this.input = input; 32 | this.generateText = generateText.asFunction({ 33 | id: `step/${this.id}/generate-text`, 34 | prompt, 35 | model, 36 | processOutput: async (output) => output, 37 | }); 38 | } 39 | 40 | protected async _execute(): Promise { 41 | const generatedText = ( 42 | await this.generateText(this.input, this.run) 43 | ).trim(); 44 | 45 | return { 46 | type: "succeeded", 47 | summary: generatedText, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/agent/src/step/Step.ts: -------------------------------------------------------------------------------- 1 | import { Run } from "../agent/Run"; 2 | import { RunContext } from "../agent/RunContext"; 3 | import { StepResult } from "./StepResult"; 4 | import { StepState } from "./StepState"; 5 | 6 | export abstract class Step { 7 | readonly id: string; 8 | readonly type: string; 9 | readonly run: Run; 10 | 11 | state: StepState; 12 | 13 | constructor({ type, run }: { type: string; run: Run }) { 14 | if (type == null) { 15 | throw new Error(`Step type is required`); 16 | } 17 | if (run == null) { 18 | throw new Error(`Step run is required`); 19 | } 20 | 21 | this.type = type; 22 | this.run = run; 23 | 24 | this.id = run.generateId({ type }); 25 | this.state = { type: "pending" }; 26 | } 27 | 28 | protected abstract _execute(context: RunContext): Promise; 29 | 30 | async execute(): Promise { 31 | const cancelCheck = this.run.checkCancel(); 32 | if (cancelCheck.shouldCancel) { 33 | return { type: "cancelled", reason: cancelCheck.reason }; 34 | } 35 | 36 | if (this.state.type !== "pending") { 37 | throw new Error(`Step is already running`); 38 | } 39 | 40 | this.state = { type: "running" }; 41 | this.run.onStepExecutionStarted({ step: this }); 42 | 43 | let result: StepResult; 44 | try { 45 | result = await this._execute(this.run); 46 | } catch (error: any) { 47 | result = { 48 | type: "failed", 49 | summary: error.message ?? "Step failed.", 50 | error, 51 | }; 52 | } 53 | 54 | this.state = result; 55 | this.run.onStepExecutionFinished({ step: this, result }); 56 | 57 | return result; 58 | } 59 | 60 | isDoneStep() { 61 | return false; 62 | } 63 | 64 | getStepCount() { 65 | return 1; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/agent/src/step/StepFactory.ts: -------------------------------------------------------------------------------- 1 | import { Run } from "../agent/Run"; 2 | import { Step } from "./Step"; 3 | 4 | export type StepFactory = ( 5 | run: Run 6 | ) => Promise>; 7 | -------------------------------------------------------------------------------- /packages/agent/src/step/StepResult.ts: -------------------------------------------------------------------------------- 1 | import { StepState } from "./StepState"; 2 | 3 | export type StepResult = StepState & { 4 | type: "cancelled" | "failed" | "succeeded"; // no pending or running any more 5 | }; 6 | -------------------------------------------------------------------------------- /packages/agent/src/step/StepState.ts: -------------------------------------------------------------------------------- 1 | export type StepState = 2 | | { 3 | type: "pending" | "running"; 4 | } 5 | | { 6 | type: "cancelled"; 7 | reason?: string; 8 | } 9 | | { 10 | type: "succeeded"; 11 | summary: string; 12 | input?: unknown; 13 | output?: unknown; 14 | } 15 | | { 16 | type: "failed"; 17 | summary: string; 18 | input?: unknown; 19 | error: unknown; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/agent/src/step/UpdateTasksLoop.ts: -------------------------------------------------------------------------------- 1 | import { Run } from "../agent/Run"; 2 | import { RunContext } from "../agent/RunContext"; 3 | import { Loop } from "./Loop"; 4 | import { Step } from "./Step"; 5 | import { StepFactory } from "./StepFactory"; 6 | import { StepResult } from "./StepResult"; 7 | 8 | export const updateTasksLoop = 9 | ({ 10 | type, 11 | initialTasks, 12 | generateExecutionStep, 13 | updateTaskList, 14 | }: { 15 | type?: string; 16 | initialTasks?: string[]; 17 | generateExecutionStep: ({}: { 18 | task: string; 19 | run: Run; 20 | }) => Step; 21 | updateTaskList: updateTaskList; 22 | }): StepFactory => 23 | async (run) => 24 | new UpdateTasksLoop({ 25 | type, 26 | initialTasks, 27 | generateExecutionStep, 28 | updateTaskList, 29 | run, 30 | }); 31 | 32 | export type updateTaskList = ( 33 | _: { 34 | runState: RUN_STATE; 35 | completedTask: string; 36 | completedTaskResult: string; 37 | remainingTasks: string[]; 38 | }, 39 | context: RunContext 40 | ) => PromiseLike>; 41 | 42 | export class UpdateTasksLoop extends Loop { 43 | tasks: Array; 44 | currentTask: string | undefined; 45 | 46 | private readonly generateExecutionStep: ({}: { 47 | task: string; 48 | run: Run; 49 | }) => Step; 50 | 51 | private readonly updateTaskList: updateTaskList; 52 | 53 | constructor({ 54 | type = "loop.update-tasks", 55 | initialTasks = ["Develop a task list."], 56 | generateExecutionStep, 57 | updateTaskList, 58 | run, 59 | }: { 60 | type?: string; 61 | initialTasks?: string[]; 62 | generateExecutionStep: ({}: { 63 | task: string; 64 | run: Run; 65 | }) => Step; 66 | updateTaskList: updateTaskList; 67 | run: Run; 68 | }) { 69 | super({ type, run }); 70 | 71 | this.tasks = initialTasks; 72 | 73 | this.updateTaskList = updateTaskList; 74 | this.generateExecutionStep = generateExecutionStep; 75 | } 76 | 77 | protected async getNextStep() { 78 | this.currentTask = this.tasks.shift()!; 79 | return this.generateExecutionStep({ 80 | task: this.currentTask, 81 | run: this.run, 82 | }); 83 | } 84 | 85 | protected hasMoreSteps(): boolean { 86 | return this.tasks.length > 0; 87 | } 88 | 89 | protected async update({ 90 | step, 91 | result, 92 | }: { 93 | step: Step; 94 | result: StepResult & { 95 | type: "succeeded" | "failed"; 96 | }; 97 | }) { 98 | this.tasks = await this.updateTaskList( 99 | { 100 | runState: this.run.state, 101 | completedTask: this.currentTask!, 102 | completedTaskResult: result.summary, 103 | remainingTasks: this.tasks, 104 | }, 105 | this.run 106 | ); 107 | this.currentTask = undefined; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/agent/src/step/createActionStep.ts: -------------------------------------------------------------------------------- 1 | import { ActionParameters } from "../action"; 2 | import { AnyAction } from "../action/Action"; 3 | import { Run } from "../agent/Run"; 4 | import { BasicToolStep } from "../tool/BasicToolStep"; 5 | import { ReflectiveToolStep } from "../tool/ReflectiveToolStep"; 6 | 7 | export async function createActionStep({ 8 | action, 9 | input, 10 | run, 11 | }: { 12 | action: AnyAction; 13 | input: ActionParameters; 14 | run: Run; 15 | }) { 16 | const actionType = action.type; 17 | switch (actionType) { 18 | case "custom-step": { 19 | return action.createStep({ input, run }); 20 | } 21 | case "basic-tool": { 22 | return new BasicToolStep({ 23 | action, 24 | input, 25 | run, 26 | }); 27 | } 28 | case "reflective-tool": { 29 | return new ReflectiveToolStep({ 30 | action, 31 | input, 32 | run, 33 | }); 34 | } 35 | default: { 36 | const unsupportedActionType: never = actionType; 37 | throw new Error(`Unsupported action type: ${unsupportedActionType}`); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/agent/src/step/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createActionStep"; 2 | export * from "./ErrorStep"; 3 | export * from "./FixedStepsLoop"; 4 | export * from "./GenerateChatCompletionFunction"; 5 | export * from "./GenerateNextStepLoop"; 6 | export * from "./NoopStep"; 7 | export * from "./PromptStep"; 8 | export * from "./Step"; 9 | export * from "./StepResult"; 10 | export * from "./StepState"; 11 | export * from "./UpdateTasksLoop"; 12 | -------------------------------------------------------------------------------- /packages/agent/src/text-store/InMemoryTextStore.ts: -------------------------------------------------------------------------------- 1 | import { RunContext } from "../agent"; 2 | import { EmbedFunction } from "../embedding/EmbedFunction"; 3 | import { cosineSimilarity } from "../util/cosineSimilarity"; 4 | import { createNextId } from "../util/createNextId"; 5 | 6 | export class InMemoryTextStore { 7 | private readonly embed: EmbedFunction; 8 | private readonly nextId = createNextId(); 9 | 10 | private readonly texts: Map< 11 | string, 12 | { 13 | id: string; 14 | embedding: number[]; 15 | text: string; 16 | } 17 | > = new Map(); 18 | 19 | constructor({ embed }: { embed: EmbedFunction }) { 20 | this.embed = embed; 21 | } 22 | 23 | async store( 24 | { text }: { text: string }, 25 | context: RunContext 26 | ): Promise<{ id: string }> { 27 | const id = this.nextId(); 28 | const embedding = await this.embed({ value: text }, context); 29 | 30 | this.texts.set(id, { id, embedding, text }); 31 | 32 | return { id }; 33 | } 34 | 35 | async retrieve( 36 | { 37 | query, 38 | maxResults = 5, 39 | similarityThreshold = 0.5, 40 | }: { query: string; maxResults?: number; similarityThreshold: number }, 41 | context: RunContext 42 | ): Promise<{ id: string; text: string; similarity: number }[]> { 43 | const queryEmbedding = await this.embed({ value: query }, context); 44 | 45 | const textSimilarities = [...this.texts.values()] 46 | .map((entry) => ({ 47 | id: entry.id, 48 | similarity: cosineSimilarity(entry.embedding, queryEmbedding), 49 | text: entry.text, 50 | })) 51 | .filter((entry) => entry.similarity > similarityThreshold); 52 | 53 | textSimilarities.sort((a, b) => b.similarity - a.similarity); 54 | 55 | return textSimilarities.slice(0, maxResults); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/agent/src/text-store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./InMemoryTextStore.js"; 2 | -------------------------------------------------------------------------------- /packages/agent/src/text/extract/ExtractFunction.ts: -------------------------------------------------------------------------------- 1 | import { RunContext } from "../../agent/RunContext"; 2 | 3 | export type ExtractFunction = ( 4 | options: { 5 | text: string; 6 | topic: string; 7 | }, 8 | context: RunContext 9 | ) => PromiseLike; 10 | -------------------------------------------------------------------------------- /packages/agent/src/text/extract/extractRecursively.ts: -------------------------------------------------------------------------------- 1 | import { RunContext } from "../../agent/RunContext"; 2 | import { SplitFunction } from "../split"; 3 | import { ExtractFunction } from "./ExtractFunction"; 4 | 5 | export async function extractRecursively({ 6 | extract, 7 | split, 8 | text, 9 | topic, 10 | context, 11 | }: { 12 | extract: ExtractFunction; 13 | split: SplitFunction; 14 | text: string; 15 | topic: string; 16 | context: RunContext; 17 | }): Promise { 18 | const chunks = await split({ text }); 19 | 20 | const extractedTexts = []; 21 | for (const chunk of chunks) { 22 | extractedTexts.push(await extract({ text: chunk, topic }, context)); 23 | } 24 | 25 | if (extractedTexts.length === 1) { 26 | return extractedTexts[0]!; 27 | } 28 | 29 | // recursive summarization: will split joined summaries as needed to stay 30 | // within the allowed size limit of the splitter. 31 | return extractRecursively({ 32 | text: extractedTexts.join("\n\n"), 33 | topic, 34 | extract, 35 | split, 36 | context, 37 | }); 38 | } 39 | 40 | extractRecursively.asExtractFunction = 41 | ({ 42 | split, 43 | extract, 44 | }: { 45 | split: SplitFunction; 46 | extract: ExtractFunction; 47 | }): ExtractFunction => 48 | async ({ text, topic }, context: RunContext) => 49 | extractRecursively({ 50 | text, 51 | topic, 52 | extract, 53 | split, 54 | context, 55 | }); 56 | -------------------------------------------------------------------------------- /packages/agent/src/text/extract/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ExtractFunction"; 2 | export * from "./extractRecursively"; 3 | export * from "./splitExtractRewrite"; 4 | -------------------------------------------------------------------------------- /packages/agent/src/text/extract/splitExtractRewrite.ts: -------------------------------------------------------------------------------- 1 | import { RunContext } from "../../agent"; 2 | import { SplitFunction } from "../split"; 3 | import { ExtractFunction } from "./ExtractFunction"; 4 | 5 | export const splitExtractRewrite = async ( 6 | { 7 | split, 8 | extract, 9 | include, 10 | rewrite, 11 | text, 12 | topic, 13 | }: { 14 | split: SplitFunction; 15 | extract: ExtractFunction; 16 | include: (text: string) => boolean; 17 | rewrite: ExtractFunction; 18 | text: string; 19 | topic: string; 20 | }, 21 | context: RunContext 22 | ) => { 23 | const chunks = await split({ text }); 24 | 25 | const extractedTexts = []; 26 | for (const chunk of chunks) { 27 | const extracted = await extract({ text: chunk, topic }, context); 28 | if (include(extracted)) { 29 | extractedTexts.push(extracted); 30 | } 31 | } 32 | 33 | return rewrite({ text: extractedTexts.join("\n\n"), topic }, context); 34 | }; 35 | 36 | splitExtractRewrite.asExtractFunction = 37 | ({ 38 | split, 39 | extract, 40 | include, 41 | rewrite, 42 | }: { 43 | split: SplitFunction; 44 | extract: ExtractFunction; 45 | include: (text: string) => boolean; 46 | rewrite: ExtractFunction; 47 | }): ExtractFunction => 48 | async ({ text, topic }, context: RunContext) => 49 | splitExtractRewrite( 50 | { 51 | split, 52 | extract, 53 | include, 54 | rewrite, 55 | text, 56 | topic, 57 | }, 58 | context 59 | ); 60 | -------------------------------------------------------------------------------- /packages/agent/src/text/generate/GeneratorModel.ts: -------------------------------------------------------------------------------- 1 | export type GeneratorModel = { 2 | vendor: string; 3 | name: string; 4 | generate: (value: PROMPT_TYPE) => PromiseLike; 5 | extractOutput: (output: RAW_OUTPUT) => PromiseLike; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/agent/src/text/generate/generate.ts: -------------------------------------------------------------------------------- 1 | import { RunContext } from "../../agent/RunContext"; 2 | import { Prompt } from "../../prompt/Prompt"; 3 | import { RetryFunction } from "../../util"; 4 | import { retryWithExponentialBackoff } from "../../util/retryWithExponentialBackoff"; 5 | import { GeneratorModel } from "./GeneratorModel"; 6 | 7 | export async function generate< 8 | INPUT, 9 | PROMPT_TYPE, 10 | RAW_OUTPUT, 11 | GENERATED_OUTPUT, 12 | OUTPUT 13 | >( 14 | { 15 | id, 16 | prompt, 17 | input, 18 | model, 19 | processOutput, 20 | retry = retryWithExponentialBackoff(), 21 | }: { 22 | id?: string | undefined; 23 | input: INPUT; 24 | prompt: Prompt; 25 | model: GeneratorModel; 26 | processOutput: (output: GENERATED_OUTPUT) => PromiseLike; 27 | retry?: RetryFunction; 28 | }, 29 | context?: RunContext 30 | ): Promise { 31 | const expandedPrompt = await prompt(input); 32 | 33 | const startTime = performance.now(); 34 | const startEpochSeconds = Math.floor( 35 | (performance.timeOrigin + startTime) / 1000 36 | ); 37 | 38 | const rawOutput = await retry(() => model.generate(expandedPrompt)); 39 | 40 | const textGenerationDurationInMs = Math.ceil(performance.now() - startTime); 41 | 42 | const metadata = { 43 | id, 44 | model: { 45 | vendor: model.vendor, 46 | name: model.name, 47 | }, 48 | startEpochSeconds, 49 | durationInMs: textGenerationDurationInMs, 50 | tries: rawOutput.tries, 51 | }; 52 | 53 | if (!rawOutput.success) { 54 | context?.recordCall?.({ 55 | type: "generate", 56 | success: false, 57 | metadata, 58 | input: expandedPrompt, 59 | error: rawOutput.error, 60 | }); 61 | 62 | throw rawOutput.error; 63 | } 64 | 65 | const extractedOutput = await model.extractOutput(rawOutput.result); 66 | 67 | context?.recordCall?.({ 68 | type: "generate", 69 | success: true, 70 | metadata, 71 | input: expandedPrompt, 72 | rawOutput: rawOutput.result, 73 | extractedOutput, 74 | }); 75 | 76 | return processOutput(extractedOutput); 77 | } 78 | 79 | generate.asFunction = 80 | ({ 81 | id, 82 | prompt, 83 | model, 84 | processOutput, 85 | retry, 86 | }: { 87 | id?: string | undefined; 88 | prompt: Prompt; 89 | model: GeneratorModel; 90 | processOutput: (output: GENERATED_OUTPUT) => PromiseLike; 91 | retry?: RetryFunction; 92 | }) => 93 | async (input: INPUT, context: RunContext) => 94 | generate( 95 | { 96 | id, 97 | prompt, 98 | input, 99 | model, 100 | processOutput, 101 | retry, 102 | }, 103 | context 104 | ); 105 | -------------------------------------------------------------------------------- /packages/agent/src/text/generate/generateText.ts: -------------------------------------------------------------------------------- 1 | import { RunContext } from "../../agent/RunContext"; 2 | import { Prompt } from "../../prompt/Prompt"; 3 | import { RetryFunction } from "../../util/RetryFunction"; 4 | import { GeneratorModel } from "./GeneratorModel"; 5 | import { generate } from "./generate"; 6 | 7 | export function generateText( 8 | { 9 | id, 10 | input, 11 | prompt, 12 | model, 13 | processOutput = async (output) => output.trim(), 14 | retry, 15 | }: { 16 | id?: string | undefined; 17 | input: INPUT; 18 | prompt: Prompt; 19 | model: GeneratorModel; 20 | processOutput?: (output: string) => PromiseLike; 21 | retry?: RetryFunction; 22 | }, 23 | context?: RunContext 24 | ) { 25 | return generate( 26 | { 27 | id, 28 | input, 29 | prompt, 30 | model, 31 | processOutput, 32 | retry, 33 | }, 34 | context 35 | ); 36 | } 37 | 38 | generateText.asFunction = 39 | ({ 40 | id, 41 | prompt, 42 | model, 43 | processOutput, 44 | retry, 45 | }: { 46 | id?: string | undefined; 47 | prompt: Prompt; 48 | model: GeneratorModel; 49 | processOutput?: (output: string) => PromiseLike; 50 | retry?: RetryFunction; 51 | }) => 52 | async (input: INPUT, context: RunContext) => 53 | generateText( 54 | { 55 | id, 56 | input, 57 | prompt, 58 | model, 59 | processOutput, 60 | retry, 61 | }, 62 | context 63 | ); 64 | -------------------------------------------------------------------------------- /packages/agent/src/text/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./extract"; 2 | export * from "./generate/GeneratorModel"; 3 | export * from "./generate/generate"; 4 | export * from "./generate/generateText"; 5 | export * from "./load"; 6 | export * from "./split"; 7 | -------------------------------------------------------------------------------- /packages/agent/src/text/load.ts: -------------------------------------------------------------------------------- 1 | export async function load({ 2 | from, 3 | using, 4 | convert, 5 | }: { 6 | from: SOURCE_PARAMETERS; 7 | using: (parameters: SOURCE_PARAMETERS) => PromiseLike; 8 | convert: (rawFormat: RAW_FORMAT) => PromiseLike; 9 | }) { 10 | return convert(await using(from)); 11 | } 12 | 13 | load.asFunction = 14 | ({ 15 | using, 16 | convert, 17 | }: { 18 | using: (parameters: SOURCE_PARAMETERS) => PromiseLike; 19 | convert: (rawFormat: RAW_FORMAT) => PromiseLike; 20 | }) => 21 | async (from: SOURCE_PARAMETERS) => 22 | load({ from, using, convert }); 23 | -------------------------------------------------------------------------------- /packages/agent/src/text/split/SplitFunction.ts: -------------------------------------------------------------------------------- 1 | export type SplitFunction = ({}: { 2 | text: string; 3 | }) => PromiseLike>; 4 | -------------------------------------------------------------------------------- /packages/agent/src/text/split/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./splitRecursively"; 2 | export * from "./SplitFunction"; 3 | -------------------------------------------------------------------------------- /packages/agent/src/text/split/splitRecursively.ts: -------------------------------------------------------------------------------- 1 | import { Tokenizer } from "../../tokenizer/Tokenizer"; 2 | import { SplitFunction } from "./SplitFunction"; 3 | 4 | function splitRecursivelyImplementation({ 5 | maxChunkSize, 6 | segments, 7 | }: { 8 | maxChunkSize: number; 9 | segments: string | Array; 10 | }): Array { 11 | if (segments.length < maxChunkSize) { 12 | return Array.isArray(segments) ? [segments.join("")] : [segments]; 13 | } 14 | 15 | const half = Math.ceil(segments.length / 2); 16 | const left = segments.slice(0, half); 17 | const right = segments.slice(half); 18 | 19 | return [ 20 | ...splitRecursivelyImplementation({ 21 | segments: left, 22 | maxChunkSize, 23 | }), 24 | ...splitRecursivelyImplementation({ 25 | segments: right, 26 | maxChunkSize, 27 | }), 28 | ]; 29 | } 30 | 31 | export const splitRecursivelyAtCharacter = async ({ 32 | maxChunkSize, 33 | text, 34 | }: { 35 | maxChunkSize: number; 36 | text: string; 37 | }) => 38 | splitRecursivelyImplementation({ 39 | maxChunkSize, 40 | segments: text, 41 | }); 42 | 43 | splitRecursivelyAtCharacter.asSplitFunction = 44 | ({ maxChunkSize }: { maxChunkSize: number }): SplitFunction => 45 | async ({ text }: { text: string }) => 46 | splitRecursivelyAtCharacter({ maxChunkSize, text }); 47 | 48 | export const splitRecursivelyAtToken = async ({ 49 | tokenizer, 50 | maxChunkSize, 51 | text, 52 | }: { 53 | tokenizer: Tokenizer; 54 | maxChunkSize: number; 55 | text: string; 56 | }) => 57 | splitRecursivelyImplementation({ 58 | maxChunkSize, 59 | segments: (await tokenizer.encode(text)).texts, 60 | }); 61 | 62 | splitRecursivelyAtToken.asSplitFunction = 63 | ({ 64 | tokenizer, 65 | maxChunkSize, 66 | }: { 67 | tokenizer: Tokenizer; 68 | maxChunkSize: number; 69 | }): SplitFunction => 70 | async ({ text }: { text: string }) => 71 | splitRecursivelyAtToken({ 72 | tokenizer, 73 | maxChunkSize, 74 | text, 75 | }); 76 | -------------------------------------------------------------------------------- /packages/agent/src/tokenizer/Tokenizer.ts: -------------------------------------------------------------------------------- 1 | export type Tokenizer = { 2 | encode: (text: string) => PromiseLike<{ 3 | tokens: Uint32Array; 4 | texts: Array; 5 | }>; 6 | decode: (tokens: Uint32Array) => PromiseLike; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/agent/src/tokenizer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Tokenizer"; 2 | -------------------------------------------------------------------------------- /packages/agent/src/tool/BasicToolStep.ts: -------------------------------------------------------------------------------- 1 | import { BasicToolAction } from "../action"; 2 | import { Run } from "../agent/Run"; 3 | import { RunContext } from "../agent/RunContext"; 4 | import { Step } from "../step/Step"; 5 | import { StepResult } from "../step/StepResult"; 6 | 7 | export class BasicToolStep< 8 | INPUT extends Record, 9 | OUTPUT, 10 | RUN_STATE 11 | > extends Step { 12 | readonly action: BasicToolAction; 13 | readonly input: INPUT; 14 | 15 | constructor({ 16 | run, 17 | action, 18 | input, 19 | }: { 20 | run: Run; 21 | action: BasicToolAction; 22 | input: INPUT; 23 | }) { 24 | super({ type: action.id, run }); 25 | 26 | this.action = action; 27 | this.input = input; 28 | } 29 | 30 | protected async _execute(context: RunContext): Promise { 31 | try { 32 | const { output, summary } = await this.action.execute( 33 | { 34 | input: this.input, 35 | action: this.action, 36 | }, 37 | context 38 | ); 39 | 40 | return { 41 | type: "succeeded", 42 | summary, 43 | input: this.input, 44 | output, 45 | }; 46 | } catch (error) { 47 | return { 48 | type: "failed", 49 | summary: (error as any)?.message ?? "Task failed.", 50 | error, 51 | }; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/agent/src/tool/ReflectiveToolStep.ts: -------------------------------------------------------------------------------- 1 | import { ReflectiveToolAction } from "../action"; 2 | import { Run } from "../agent/Run"; 3 | import { RunContext } from "../agent/RunContext"; 4 | import { Step } from "../step/Step"; 5 | import { StepResult } from "../step/StepResult"; 6 | 7 | export class ReflectiveToolStep< 8 | INPUT extends Record, 9 | OUTPUT, 10 | RUN_STATE 11 | > extends Step { 12 | readonly action: ReflectiveToolAction; 13 | readonly input: INPUT; 14 | 15 | constructor({ 16 | run, 17 | action, 18 | input, 19 | }: { 20 | run: Run; 21 | action: ReflectiveToolAction; 22 | input: INPUT; 23 | }) { 24 | super({ type: action.id, run }); 25 | 26 | this.action = action; 27 | this.input = input; 28 | } 29 | 30 | protected async _execute(context: RunContext): Promise { 31 | try { 32 | const { output, summary } = await this.action.execute( 33 | { 34 | input: this.input, 35 | action: this.action, 36 | run: this.run, 37 | }, 38 | context 39 | ); 40 | 41 | return { 42 | type: "succeeded", 43 | summary, 44 | input: this.input, 45 | output, 46 | }; 47 | } catch (error) { 48 | return { 49 | type: "failed", 50 | summary: (error as any)?.message ?? "Task failed.", 51 | error, 52 | }; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/agent/src/tool/ask-user/AskUserTool.ts: -------------------------------------------------------------------------------- 1 | import readline from "readline"; 2 | import zod from "zod"; 3 | import { BasicToolAction, FormatResultFunction } from "../../action"; 4 | import { ExecuteBasicToolFunction } from "../../action/ExecuteBasicToolFunction"; 5 | 6 | export type AskUserInput = { 7 | query: string; 8 | }; 9 | 10 | export type AskUserOutput = { 11 | response: string; 12 | }; 13 | 14 | export const askUser = ({ 15 | id = "ask-user", 16 | description = "Ask the user for input or to take an action.", 17 | inputExample = { 18 | query: "{question or action description}", 19 | }, 20 | execute, 21 | formatResult = ({ input, output: { response } }) => 22 | `${input.query}: ${response}`, 23 | }: { 24 | id?: string; 25 | description?: string; 26 | inputExample?: AskUserInput; 27 | execute: ExecuteBasicToolFunction; 28 | formatResult?: FormatResultFunction; 29 | }): BasicToolAction => ({ 30 | type: "basic-tool", 31 | id, 32 | description, 33 | inputSchema: zod.object({ 34 | query: zod.string(), 35 | }), 36 | outputSchema: zod.object({ 37 | response: zod.string(), 38 | }), 39 | inputExample, 40 | execute, 41 | formatResult, 42 | }); 43 | 44 | export const executeAskUser = 45 | (): ExecuteBasicToolFunction => 46 | async ({ input: { query } }) => { 47 | const userInput = readline.createInterface({ 48 | input: process.stdin, 49 | output: process.stdout, 50 | }); 51 | 52 | const response = await new Promise((resolve) => { 53 | userInput.question(query, (answer) => { 54 | resolve(answer); 55 | userInput.close(); 56 | }); 57 | }); 58 | 59 | return { 60 | summary: `User response: ${response}`, 61 | output: { response }, 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /packages/agent/src/tool/executeRemoteTool.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { BasicToolAction } from "../action/Action"; 3 | import { ActionParameters } from "../action/ActionParameters"; 4 | import { ExecuteBasicToolFunction } from "../action/ExecuteBasicToolFunction"; 5 | 6 | export const executeRemoteTool = 7 | ({ 8 | baseUrl, 9 | }: { 10 | baseUrl: string; 11 | }): ExecuteBasicToolFunction => 12 | async ({ 13 | input, 14 | action, 15 | }: { 16 | input: INPUT; 17 | action: BasicToolAction; 18 | }): Promise<{ output: OUTPUT; summary: string }> => { 19 | try { 20 | const parametersJson = JSON.stringify(input); 21 | 22 | const response = await axios.post( 23 | `${baseUrl}/tool/${action.id}`, // TODO flexible location 24 | parametersJson, 25 | { 26 | headers: { 27 | "Content-Type": "application/json", 28 | }, 29 | } 30 | ); 31 | 32 | const result = response.data; // data is already parsed JSON 33 | 34 | // TODO safely parse + zod validate 35 | // const resultSchema: zod.Schema<{ 36 | // summary: string; 37 | // output: OUTPUT; 38 | // }> = zod.object({ 39 | // summary: zod.string(), 40 | // output: action.outputSchema, 41 | // }); 42 | 43 | return result as { 44 | summary: string; 45 | output: OUTPUT; 46 | }; // TODO remove 47 | } catch (error: any) { 48 | // TODO better error handling 49 | console.error("Error sending command:", error.message); 50 | if (axios.isAxiosError(error)) { 51 | console.error( 52 | "Axios error details:", 53 | error.response?.data, 54 | error.response?.status, 55 | error.response?.headers 56 | ); 57 | } 58 | return { 59 | summary: "Error executing command", 60 | output: error.response?.data, 61 | }; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /packages/agent/src/tool/executor/ToolRegistry.ts: -------------------------------------------------------------------------------- 1 | import { BasicToolAction } from "../../action"; 2 | 3 | export class ToolRegistry { 4 | private readonly tools: Map> = new Map(); 5 | 6 | constructor({ tools }: { tools: BasicToolAction[] }) { 7 | for (const tool of tools) { 8 | this.register(tool); 9 | } 10 | } 11 | 12 | register(tool: BasicToolAction) { 13 | if (this.tools.has(tool.id)) { 14 | throw new Error( 15 | `A tool with the id '${tool.id}' has already been registered.` 16 | ); 17 | } 18 | 19 | this.tools.set(tool.id, tool); 20 | } 21 | 22 | get toolIds() { 23 | return Array.from(this.tools.keys()); 24 | } 25 | 26 | getTool(id: string) { 27 | const tool = this.tools.get(id); 28 | 29 | if (!tool) { 30 | throw new Error( 31 | `No tool '${id}' found. Available tools: ${this.toolIds.join(", ")}` 32 | ); 33 | } 34 | 35 | return tool; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/agent/src/tool/executor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./runToolExecutor"; 2 | export * from "./toolPlugin"; 3 | -------------------------------------------------------------------------------- /packages/agent/src/tool/executor/runToolExecutor.ts: -------------------------------------------------------------------------------- 1 | import Fastify from "fastify"; 2 | import hyperid from "hyperid"; 3 | import pino from "pino"; 4 | import zod from "zod"; 5 | import { gracefullyShutdownOnSigTermAndSigInt } from "../../util/gracefullyShutdownOnSigTermAndSigInt"; 6 | import { ToolRegistry } from "./ToolRegistry"; 7 | import { createToolPlugin } from "./toolPlugin"; 8 | import { BasicToolAction } from "../../action/Action"; 9 | 10 | export const runToolExecutor = async ({ 11 | tools, 12 | }: { 13 | tools: Array>; 14 | }) => { 15 | const environmentSchema = zod.object({ 16 | WORKSPACE: zod.string(), 17 | HOST: zod.string(), 18 | PORT: zod.string(), 19 | }); 20 | 21 | const environment = environmentSchema.parse(process.env); 22 | 23 | const logger = pino({ 24 | level: "debug", 25 | messageKey: "message", 26 | }); 27 | 28 | const server = Fastify({ 29 | logger, 30 | genReqId: hyperid(), 31 | requestIdLogLabel: "requestId", 32 | }); 33 | 34 | server.register( 35 | createToolPlugin({ 36 | toolRegistry: new ToolRegistry({ tools }), 37 | logger, 38 | }) 39 | ); 40 | 41 | await server.listen({ 42 | host: environment.HOST, 43 | port: parseInt(environment.PORT), 44 | }); 45 | 46 | logger.info(`Executor service started.`); 47 | 48 | // catch uncaught exceptions (to prevent the process from crashing) 49 | process.on("uncaughtException", (error) => { 50 | logger.error(error, "Uncaught error."); 51 | }); 52 | 53 | gracefullyShutdownOnSigTermAndSigInt({ 54 | logger, 55 | async shutdown() { 56 | await server.close(); // wait for requests to be finished 57 | }, 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/agent/src/tool/executor/toolPlugin.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from "fastify"; 2 | import { pino } from "pino"; 3 | import { ToolRegistry } from "./ToolRegistry"; 4 | 5 | export function createToolPlugin({ 6 | toolRegistry, 7 | logger, 8 | }: { 9 | toolRegistry: ToolRegistry; 10 | logger: pino.Logger; 11 | }) { 12 | return async function toolPlugin(fastify: FastifyInstance) { 13 | fastify.post<{ Params: { toolType: string } }>("/tool/:toolType", { 14 | async handler(request, reply) { 15 | try { 16 | const toolType = request.params.toolType; 17 | const tool = toolRegistry.getTool(toolType); 18 | 19 | const input = tool.inputSchema.parse(request.body); 20 | 21 | const output = await tool.execute( 22 | { 23 | input, 24 | action: tool, 25 | }, 26 | null 27 | ); 28 | 29 | const textOutput = JSON.stringify(output); 30 | 31 | reply.status(200).send(textOutput); 32 | } catch (error: any) { 33 | logger.error( 34 | error, 35 | "An error occurred while processing the command." 36 | ); 37 | 38 | reply.status(500).send({ 39 | message: "An error occurred while processing the command.", 40 | error: error.message, 41 | }); 42 | } 43 | }, 44 | }); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /packages/agent/src/tool/extract-information-from-webpage/ExtractInformationFromWebpageTool.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | import { FormatResultFunction } from "../../action/FormatResultFunction"; 3 | import { RunContext } from "../../agent/RunContext"; 4 | import { htmlToText } from "../../convert/htmlToText"; 5 | import { webpageAsHtmlText } from "../../source"; 6 | import { ExtractFunction, load } from "../../text"; 7 | import { ExecuteBasicToolFunction } from "../../action/ExecuteBasicToolFunction"; 8 | import { BasicToolAction } from "../../action"; 9 | 10 | export type ExtractInformationFromWebpageInput = { 11 | topic: string; 12 | url: string; 13 | }; 14 | 15 | export type ExtractInformationFromWebpageOutput = { 16 | extractedInformation: string; 17 | }; 18 | 19 | export const extractInformationFromWebpage = ({ 20 | id = "extract-information-from-webpage", 21 | description = "Extract information from a webpage considering a topic.", 22 | inputExample = { 23 | topic: "{information that I want to extract from the webpage}", 24 | url: "{https://www.example.com}", 25 | }, 26 | execute, 27 | formatResult = ({ input, output: { extractedInformation } }) => 28 | `## Extracted information on topic '${input.topic}' from ${input.url}\n${extractedInformation}`, 29 | }: { 30 | id?: string; 31 | description?: string; 32 | inputExample?: ExtractInformationFromWebpageInput; 33 | execute: ExecuteBasicToolFunction< 34 | ExtractInformationFromWebpageInput, 35 | ExtractInformationFromWebpageOutput 36 | >; 37 | formatResult?: FormatResultFunction< 38 | ExtractInformationFromWebpageInput, 39 | ExtractInformationFromWebpageOutput 40 | >; 41 | }): BasicToolAction< 42 | ExtractInformationFromWebpageInput, 43 | ExtractInformationFromWebpageOutput 44 | > => ({ 45 | type: "basic-tool", 46 | id, 47 | description, 48 | inputSchema: zod.object({ 49 | topic: zod.string(), 50 | url: zod.string(), 51 | }), 52 | outputSchema: zod.object({ 53 | extractedInformation: zod.string(), 54 | }), 55 | inputExample, 56 | execute, 57 | formatResult, 58 | }); 59 | 60 | export const executeExtractInformationFromWebpage = 61 | ({ 62 | loadText = load.asFunction({ 63 | using: webpageAsHtmlText.asFunction(), 64 | convert: htmlToText.asFunction(), 65 | }), 66 | extract, 67 | }: { 68 | loadText?: (options: { url: string }) => PromiseLike; 69 | extract: ExtractFunction; 70 | }): ExecuteBasicToolFunction< 71 | ExtractInformationFromWebpageInput, 72 | ExtractInformationFromWebpageOutput 73 | > => 74 | async ( 75 | { input: { topic, url } }: { input: ExtractInformationFromWebpageInput }, 76 | context: RunContext 77 | ) => ({ 78 | summary: `Extracted information on topic ${topic} from website ${url}.`, 79 | output: { 80 | extractedInformation: await extract( 81 | { 82 | text: await loadText({ url }), 83 | topic, 84 | }, 85 | context 86 | ), 87 | }, 88 | }); 89 | -------------------------------------------------------------------------------- /packages/agent/src/tool/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ask-user/AskUserTool"; 2 | export * from "./executeRemoteTool"; 3 | export * as executor from "./executor"; 4 | export * from "./extract-information-from-webpage/ExtractInformationFromWebpageTool"; 5 | export * from "./programmable-google-search-engine/ProgrammableGoogleSearchEngineTool"; 6 | export * from "./read-file/ReadFileTool"; 7 | export * from "./run-command/RunCommandTool"; 8 | export * from "./update-run-string-property/UpdateRunStringPropertyTool"; 9 | export * from "./write-file/WriteFileTool"; 10 | -------------------------------------------------------------------------------- /packages/agent/src/tool/programmable-google-search-engine/ProgrammableGoogleSearchEngineTool.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import zod from "zod"; 3 | import { FormatResultFunction } from "../../action/FormatResultFunction"; 4 | import { ExecuteBasicToolFunction } from "../../action/ExecuteBasicToolFunction"; 5 | import { BasicToolAction } from "../../action"; 6 | 7 | export type ProgrammableGoogleSearchEngineInput = { 8 | query: string; 9 | }; 10 | 11 | export type ProgrammableGoogleSearchEngineOutput = { 12 | results: Array<{ 13 | title: string; 14 | link: string; 15 | snippet: string; 16 | }>; 17 | }; 18 | 19 | export const programmableGoogleSearchEngineAction = ({ 20 | id = "search", 21 | description = "Search programmable Google search engine.", 22 | inputExample = { 23 | query: "{search query}", 24 | }, 25 | execute, 26 | formatResult = ({ summary, output: { results } }) => 27 | `## ${summary}\n${results 28 | .map( 29 | (result) => `### ${result.title}\n${result.link}\n${result.snippet}\n` 30 | ) 31 | .join("\n")}`, 32 | }: { 33 | id?: string; 34 | description?: string; 35 | inputExample?: ProgrammableGoogleSearchEngineInput; 36 | execute: ExecuteBasicToolFunction< 37 | ProgrammableGoogleSearchEngineInput, 38 | ProgrammableGoogleSearchEngineOutput 39 | >; 40 | formatResult?: FormatResultFunction< 41 | ProgrammableGoogleSearchEngineInput, 42 | ProgrammableGoogleSearchEngineOutput 43 | >; 44 | }): BasicToolAction< 45 | ProgrammableGoogleSearchEngineInput, 46 | ProgrammableGoogleSearchEngineOutput 47 | > => ({ 48 | type: "basic-tool", 49 | id, 50 | description, 51 | inputSchema: zod.object({ 52 | query: zod.string(), 53 | }), 54 | outputSchema: zod.object({ 55 | results: zod.array( 56 | zod.object({ 57 | title: zod.string(), 58 | link: zod.string(), 59 | snippet: zod.string(), 60 | }) 61 | ), 62 | }), 63 | inputExample, 64 | execute, 65 | formatResult, 66 | }); 67 | 68 | export const executeProgrammableGoogleSearchEngineAction = 69 | ({ 70 | key, 71 | cx, 72 | maxResults = 5, 73 | }: { 74 | key: string; 75 | cx: string; 76 | maxResults?: number; 77 | }): ExecuteBasicToolFunction< 78 | ProgrammableGoogleSearchEngineInput, 79 | ProgrammableGoogleSearchEngineOutput 80 | > => 81 | async ({ input: { query } }) => { 82 | const result = await axios.get( 83 | `https://www.googleapis.com/customsearch/v1/siterestrict?key=${key}&cx=${cx}&q=${query}` 84 | ); 85 | 86 | const items = result.data.items.slice(0, maxResults); 87 | 88 | return { 89 | summary: `Found ${items.length} search results.`, 90 | output: { 91 | results: items.map((item: any) => ({ 92 | title: item.title, 93 | link: item.link, 94 | snippet: item.snippet, 95 | })), 96 | }, 97 | }; 98 | }; 99 | -------------------------------------------------------------------------------- /packages/agent/src/tool/read-file/ReadFileTool.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import zod from "zod"; 4 | import { FormatResultFunction } from "../../action/FormatResultFunction"; 5 | import { ExecuteBasicToolFunction } from "../../action/ExecuteBasicToolFunction"; 6 | import { BasicToolAction } from "../../action"; 7 | 8 | export type ReadFileInput = { 9 | filePath: string; 10 | }; 11 | 12 | export type ReadFileOutput = { 13 | content?: string; 14 | error?: string; 15 | }; 16 | 17 | export const readFile = ({ 18 | id = "read-file", 19 | description = "Read file content.", 20 | inputExample = { 21 | filePath: "{file path relative to the workspace folder}", 22 | }, 23 | execute, 24 | formatResult = ({ summary, output: { content, error } }) => 25 | error 26 | ? `## ${summary}\n### Error\n${error}` 27 | : `## ${summary}\n### File content\n${content}`, 28 | }: { 29 | id?: string; 30 | description?: string; 31 | inputExample?: ReadFileInput; 32 | execute: ExecuteBasicToolFunction; 33 | formatResult?: FormatResultFunction; 34 | }): BasicToolAction => ({ 35 | type: "basic-tool", 36 | id, 37 | description, 38 | inputSchema: zod.object({ 39 | filePath: zod.string(), 40 | }), 41 | outputSchema: zod.object({ 42 | content: zod.string().optional(), 43 | error: zod.string().optional(), 44 | }), 45 | inputExample, 46 | execute, 47 | formatResult, 48 | }); 49 | 50 | export const executeReadFile = 51 | ({ 52 | workspacePath, 53 | }: { 54 | workspacePath: string; 55 | }): ExecuteBasicToolFunction => 56 | async ({ input: { filePath } }) => { 57 | const fullPath = path.join(workspacePath, filePath); 58 | try { 59 | const content = await fs.readFile(fullPath, "utf-8"); 60 | 61 | return { 62 | summary: `Read file ${filePath}`, 63 | output: { content }, 64 | }; 65 | } catch (error: any) { 66 | if (error.code === "ENOENT") { 67 | const { dir, files } = await getAvailableFiles(workspacePath, filePath); 68 | return { 69 | summary: `File not found.`, 70 | output: { 71 | error: `Available files and directories in ${ 72 | dir + path.sep 73 | }: ${files.join(", ")}`, 74 | }, 75 | }; 76 | } else { 77 | throw error; 78 | } 79 | } 80 | }; 81 | 82 | async function getAvailableFiles(workspacePath: string, filePath: string) { 83 | // Find lowest existing directory 84 | const directories = filePath 85 | .split(path.sep) 86 | .slice(undefined, -1) // remove the file name 87 | .filter((directory) => directory !== ""); 88 | let currentPath = ""; 89 | for (const directory of directories) { 90 | try { 91 | await fs.access(path.join(workspacePath, currentPath, directory)); 92 | currentPath = path.join(currentPath, directory); 93 | } catch (error: any) { 94 | if (error.code !== "ENOENT") { 95 | throw error; 96 | } 97 | } 98 | } 99 | 100 | // List files and directories 101 | const files = await fs.readdir(path.join(workspacePath, currentPath)); 102 | return { 103 | dir: currentPath, 104 | files: await Promise.all( 105 | files.map(async (file) => { 106 | const stat = await fs.stat(path.resolve(workspacePath, file)); 107 | return stat.isDirectory() 108 | ? path.join(currentPath, file) + path.sep 109 | : path.join(currentPath, file); 110 | }) 111 | ), 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /packages/agent/src/tool/run-command/RunCommandTool.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import zod from "zod"; 3 | import { FormatResultFunction } from "../../action/FormatResultFunction"; 4 | import { ExecuteBasicToolFunction } from "../../action/ExecuteBasicToolFunction"; 5 | import { BasicToolAction } from "../../action"; 6 | 7 | export type RunCommandInput = { 8 | command: string; 9 | }; 10 | 11 | export type RunCommandOutput = { 12 | stdout: string; 13 | stderr: string; 14 | }; 15 | 16 | export const runCommand = ({ 17 | id = "run-command", 18 | description = `Run a shell command. The output is shown. Useful commands include: 19 | - ls: list files 20 | - mv: move and rename files`, 21 | inputExample = { 22 | command: "{command}", 23 | }, 24 | execute, 25 | formatResult = ({ summary, output: { stdout, stderr } }) => { 26 | const stdoutText = 27 | stdout.trim() !== "" ? `\n### stdout\n${stdout.trim()}` : ""; 28 | 29 | const stderrText = 30 | stderr.trim() !== "" ? `\n### stderr\n${stderr.trim()}` : ""; 31 | 32 | return `## ${summary}${stdoutText}${stderrText}`; 33 | }, 34 | }: { 35 | id?: string; 36 | description?: string; 37 | inputExample?: RunCommandInput; 38 | execute: ExecuteBasicToolFunction; 39 | formatResult?: FormatResultFunction; 40 | }): BasicToolAction => ({ 41 | type: "basic-tool", 42 | id, 43 | description, 44 | inputSchema: zod.object({ 45 | command: zod.string(), 46 | }), 47 | outputSchema: zod.object({ 48 | stdout: zod.string(), 49 | stderr: zod.string(), 50 | }), 51 | inputExample, 52 | execute, 53 | formatResult, 54 | }); 55 | 56 | export const executeRunCommand = 57 | ({ 58 | workspacePath, 59 | }: { 60 | workspacePath: string; 61 | }): ExecuteBasicToolFunction => 62 | async ({ input: { command } }: { input: RunCommandInput }) => { 63 | const { stdout, stderr } = await new Promise<{ 64 | stdout: string; 65 | stderr: string; 66 | }>((resolve, reject) => { 67 | exec(command, { cwd: workspacePath }, (error, stdout, stderr) => { 68 | resolve({ 69 | stdout, 70 | stderr, 71 | }); 72 | }); 73 | }); 74 | 75 | return { 76 | summary: `Command ${command} executed successfully`, 77 | output: { 78 | stdout, 79 | stderr, 80 | }, 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /packages/agent/src/tool/update-run-string-property/UpdateRunStringPropertyTool.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | import { ReflectiveToolAction } from "../../action/Action"; 3 | import { FormatResultFunction } from "../../action/FormatResultFunction"; 4 | 5 | export type UpdateRunStringPropertyInput = { 6 | content: string; 7 | }; 8 | 9 | export type UpdateRunStringPropertyOutput = { 10 | content: string; 11 | }; 12 | 13 | export const updateRunStringProperty = < 14 | KEY extends string, 15 | RUN_STATE extends { 16 | [key in KEY]: string; 17 | } 18 | >({ 19 | id = "update-run-string-property", 20 | description = "Update a stored string value.", 21 | inputExample = { 22 | content: "{new content}", 23 | }, 24 | formatResult = ({ summary, output: { content } }) => 25 | `## ${summary}\n### New content\n${content}`, 26 | property, 27 | }: { 28 | id?: string; 29 | description?: string; 30 | inputExample?: UpdateRunStringPropertyInput; 31 | formatResult?: FormatResultFunction< 32 | UpdateRunStringPropertyInput, 33 | UpdateRunStringPropertyOutput 34 | >; 35 | property: KEY; 36 | }): ReflectiveToolAction< 37 | UpdateRunStringPropertyInput, 38 | UpdateRunStringPropertyOutput, 39 | RUN_STATE 40 | > => ({ 41 | type: "reflective-tool", 42 | id, 43 | description, 44 | inputSchema: zod.object({ 45 | content: zod.string(), 46 | }), 47 | outputSchema: zod.object({ 48 | content: zod.string(), 49 | }), 50 | inputExample, 51 | formatResult, 52 | execute: async ({ input: { content }, run }) => { 53 | // @ts-expect-error TODO investigate constraint issue 54 | run.state[property] = content; 55 | 56 | return { 57 | output: { 58 | content, 59 | }, 60 | summary: `Updated ${property}.`, 61 | }; 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /packages/agent/src/tool/write-file/WriteFileTool.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import zod from "zod"; 4 | import { FormatResultFunction } from "../../action/FormatResultFunction"; 5 | import { ExecuteBasicToolFunction } from "../../action/ExecuteBasicToolFunction"; 6 | import { BasicToolAction } from "../../action/Action"; 7 | 8 | export type WriteFileInput = { 9 | filePath: string; 10 | content: string; 11 | }; 12 | 13 | export type WriteFileOutput = { 14 | content: string; 15 | }; 16 | 17 | export const writeFile = ({ 18 | id = "write-file", 19 | description = "Write file content.", 20 | inputExample = { 21 | filePath: "{file path relative to the workspace folder}", 22 | content: "{new file content}", 23 | }, 24 | execute, 25 | formatResult = ({ summary, output: { content } }) => 26 | `## ${summary}\n### New file content\n${content}`, 27 | }: { 28 | id?: string; 29 | description?: string; 30 | inputExample?: WriteFileInput; 31 | execute: ExecuteBasicToolFunction; 32 | formatResult?: FormatResultFunction; 33 | }): BasicToolAction => ({ 34 | type: "basic-tool", 35 | id, 36 | description, 37 | inputSchema: zod.object({ 38 | filePath: zod.string(), 39 | content: zod.string(), 40 | }), 41 | outputSchema: zod.object({ 42 | content: zod.string(), 43 | }), 44 | inputExample, 45 | execute, 46 | formatResult, 47 | }); 48 | 49 | export const executeWriteFile = 50 | ({ 51 | workspacePath, 52 | }: { 53 | workspacePath: string; 54 | }): ExecuteBasicToolFunction => 55 | async ({ input: { filePath, content } }) => { 56 | // TODO try-catch 57 | const fullPath = path.join(workspacePath, filePath); 58 | await fs.writeFile(fullPath, content); 59 | const newContent = await fs.readFile(fullPath, "utf-8"); 60 | 61 | return { 62 | summary: `Replaced the content of file ${filePath}.`, 63 | output: { content: newContent }, 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /packages/agent/src/util/RetryFunction.ts: -------------------------------------------------------------------------------- 1 | export type RetryFunction = (f: () => PromiseLike) => Promise< 2 | { 3 | tries: number; 4 | } & ( 5 | | { 6 | success: true; 7 | result: T; 8 | } 9 | | { 10 | success: false; 11 | error: unknown; 12 | } 13 | ) 14 | >; 15 | -------------------------------------------------------------------------------- /packages/agent/src/util/cosineSimilarity.ts: -------------------------------------------------------------------------------- 1 | export function cosineSimilarity(a: number[], b: number[]) { 2 | if (a.length !== b.length) { 3 | throw new Error( 4 | `Vectors must have the same length (a: ${a.length}, b: ${b.length})` 5 | ); 6 | } 7 | 8 | return dotProduct(a, b) / (magnitude(a) * magnitude(b)); 9 | } 10 | 11 | function dotProduct(a: number[], b: number[]) { 12 | return a.reduce( 13 | (acc: number, val: number, i: number) => acc + val * b[i]!, 14 | 0 15 | ); 16 | } 17 | 18 | function magnitude(a: number[]) { 19 | return Math.sqrt(dotProduct(a, a)); 20 | } 21 | -------------------------------------------------------------------------------- /packages/agent/src/util/createNextId.test.ts: -------------------------------------------------------------------------------- 1 | import { createNextId } from "./createNextId"; 2 | 3 | test("createNextId generates sequential IDs", () => { 4 | const nextId = createNextId(); 5 | expect(nextId()).toBe("0"); 6 | expect(nextId()).toBe("1"); 7 | expect(nextId()).toBe("2"); 8 | }); 9 | 10 | test("createNextId starts from the given start value", () => { 11 | const nextId = createNextId(5); 12 | expect(nextId()).toBe("5"); 13 | expect(nextId()).toBe("6"); 14 | expect(nextId()).toBe("7"); 15 | }); 16 | 17 | test("createNextId handles large start values", () => { 18 | const nextId = createNextId(Number.MAX_SAFE_INTEGER - 2); 19 | expect(nextId()).toBe(`${Number.MAX_SAFE_INTEGER - 2}`); 20 | expect(nextId()).toBe(`${Number.MAX_SAFE_INTEGER - 1}`); 21 | expect(nextId()).toBe(`${Number.MAX_SAFE_INTEGER}`); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/agent/src/util/createNextId.ts: -------------------------------------------------------------------------------- 1 | export function createNextId(start: number = 0) { 2 | let id = start; 3 | return () => `${id++}`; 4 | } 5 | -------------------------------------------------------------------------------- /packages/agent/src/util/gracefullyShutdownOnSigTermAndSigInt.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "pino"; 2 | 3 | /** 4 | * Listens for SIGINT and SIGTERM and terminates the process gracefully. 5 | * 6 | * This ensures that running requests, database connections and other operations 7 | * are finished before the process is terminated. 8 | */ 9 | export function gracefullyShutdownOnSigTermAndSigInt({ 10 | logger, 11 | shutdown, 12 | }: { 13 | logger: Logger; 14 | shutdown: () => Promise; 15 | }) { 16 | async function closeGracefully(signal: NodeJS.Signals) { 17 | logger.info(`Received signal to terminate: ${signal}`); 18 | 19 | await shutdown(); 20 | 21 | process.kill(process.pid, signal); 22 | } 23 | 24 | process.once("SIGINT", closeGracefully); 25 | process.once("SIGTERM", closeGracefully); 26 | } 27 | -------------------------------------------------------------------------------- /packages/agent/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RetryFunction.js"; 2 | export * from "./cosineSimilarity.js"; 3 | export * from "./createNextId.js"; 4 | export * from "./retryWithExponentialBackoff.js"; 5 | -------------------------------------------------------------------------------- /packages/agent/src/util/retryWithExponentialBackoff.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { RetryFunction } from "./RetryFunction"; 3 | 4 | export const retryWithExponentialBackoff = 5 | ({ maxTries = 5, delay = 2000 } = {}): RetryFunction => 6 | async (f: () => PromiseLike) => 7 | _retryWithExponentialBackoff(f, { maxTries, delay }); 8 | 9 | export const retryNever = () => retryWithExponentialBackoff({ maxTries: 1 }); 10 | 11 | async function _retryWithExponentialBackoff( 12 | f: () => PromiseLike, 13 | { maxTries = 5, delay = 2000 } = {}, 14 | tryNumber = 1 15 | ): Promise< 16 | { 17 | tries: number; 18 | } & ( 19 | | { 20 | success: true; 21 | result: T; 22 | } 23 | | { 24 | success: false; 25 | error: unknown; 26 | } 27 | ) 28 | > { 29 | try { 30 | return { 31 | success: true, 32 | tries: tryNumber, 33 | result: await f(), 34 | }; 35 | } catch (error) { 36 | if ( 37 | axios.isAxiosError(error) && 38 | (error.response?.status === 429 || // too many requests 39 | error.response?.status === 502 || 40 | error.response?.status === 520) && // cloudflare error 41 | maxTries > tryNumber 42 | ) { 43 | await new Promise((resolve) => setTimeout(resolve, delay)); 44 | return _retryWithExponentialBackoff( 45 | f, 46 | { maxTries, delay: 2 * delay }, 47 | tryNumber + 1 48 | ); 49 | } 50 | 51 | return { 52 | success: false, 53 | tries: tryNumber, 54 | error, 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noUncheckedIndexedAccess": true, 5 | "declaration": true, 6 | "sourceMap": true, 7 | "target": "es2020", 8 | "lib": ["es2020", "dom"], 9 | "module": "commonjs", 10 | "types": ["node", "jest"], 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "moduleResolution": "node", 14 | "rootDir": "./src", 15 | "outDir": "./.build/js" 16 | }, 17 | "include": ["src/**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "examples/*" 3 | - "packages/*" 4 | --------------------------------------------------------------------------------