@aj-archipelago/cortex 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.sample +1 -0
- package/LICENSE +25 -0
- package/README.md +224 -0
- package/config/default.json +1 -0
- package/config.js +168 -0
- package/graphql/chunker.js +147 -0
- package/graphql/graphql.js +183 -0
- package/graphql/parser.js +58 -0
- package/graphql/pathwayPrompter.js +145 -0
- package/graphql/pathwayResolver.js +250 -0
- package/graphql/pathwayResponseParser.js +24 -0
- package/graphql/prompt.js +45 -0
- package/graphql/pubsub.js +4 -0
- package/graphql/resolver.js +43 -0
- package/graphql/subscriptions.js +21 -0
- package/graphql/typeDef.js +48 -0
- package/index.js +7 -0
- package/lib/keyValueStorageClient.js +33 -0
- package/lib/promiser.js +24 -0
- package/lib/request.js +51 -0
- package/package.json +49 -0
- package/pathways/basePathway.js +23 -0
- package/pathways/bias.js +3 -0
- package/pathways/chat.js +12 -0
- package/pathways/complete.js +3 -0
- package/pathways/edit.js +4 -0
- package/pathways/entities.js +9 -0
- package/pathways/index.js +12 -0
- package/pathways/paraphrase.js +3 -0
- package/pathways/sentiment.js +3 -0
- package/pathways/summary.js +43 -0
- package/pathways/topics.js +9 -0
- package/pathways/translate.js +9 -0
- package/start.js +3 -0
- package/tests/chunking.test.js +144 -0
- package/tests/main.test.js +106 -0
- package/tests/translate.test.js +118 -0
package/.env.sample
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AZURE_OAI_API_KEY=_______________________
|
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
<<<<<<< HEAD
|
|
4
|
+
Copyright (c) 2022 Al Jazeera Media Network
|
|
5
|
+
=======
|
|
6
|
+
Copyright (c) 2023 aj-archipelago
|
|
7
|
+
>>>>>>> b78a6667176ebfd09d2c7ed1549d8dc18691c344
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Cortex
|
|
2
|
+
Cortex is an extensible and open-source caching GraphQL API that provides an abstraction layer for interacting with modern natural language AI models. It simplifies and accelerates the task of querying NL AI models (e.g. LLMs like GPT-3) by providing a structured interface to the largely unstructured world of AI prompting.
|
|
3
|
+
|
|
4
|
+
## Why build Cortex?
|
|
5
|
+
Using modern NL AI models can be complex and costly. Most models require precisely formatted, carefully engineered and sequenced prompts to produce consistent results, and the responses are typically largely unstructured text without validation or formatting. Additionally, these models are evolving rapidly, are typically costly and slow to query and implement hard request size and rate restrictions that need to be carefully navigated for optimum throughput. Cortex offers a solution to these problems and provides a simple and extensible package for interacting with NL AI models.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* Simple architecture to build functional endpoints (called `pathways`), that implement common NL AI tasks. Included core pathways include chat, summarization, translation, paraphrasing, completion, spelling and grammar correction, entity extraction, topic classification, sentiment analysis, and bias analysis.
|
|
10
|
+
* Allows for building multi-model, multi-vendor, and model-agnostic pathways (choose the right model or combination of models for the job, implement redundancy)
|
|
11
|
+
* Easy, templatized prompt definition with flexible support for most prompt engineering techniques and strategies ranging from simple, single prompts to complex prompt chains with context continuity.
|
|
12
|
+
* Integrated context persistence: have your pathways "remember" whatever you want and use it on the next request to the model
|
|
13
|
+
* Automatic traffic management and content optimization: configurable model-specific input chunking, request parallelization, rate limiting, and chunked response aggregation
|
|
14
|
+
* Extensible parsing and validation of input data - protect your model calls from bad inputs or filter prompt injection attempts.
|
|
15
|
+
* Extensible parsing and validation of return data - return formatted objects to your application instead of just string blobs!
|
|
16
|
+
* Caching of repeated queries to provide instant results and avoid excess requests to the underlying model in repetitive use cases (chat bots, unit tests, etc.)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
In order to use Cortex, you must first have a working Node.js environment. The version of Node.js should be at least 14 or higher. After verifying that you have the correct version of Node.js installed, you can get the simplest form up and running with a couple of commands.
|
|
20
|
+
## Quick Start
|
|
21
|
+
```sh
|
|
22
|
+
git clone git@github.com:aj-archipelago/cortex.git
|
|
23
|
+
npm install
|
|
24
|
+
export OPENAI_API_KEY=<your key>
|
|
25
|
+
npm start
|
|
26
|
+
```
|
|
27
|
+
Yup, that's it, at least in the simplest possible case. That will get you access to all of the built in pathways.
|
|
28
|
+
## Using Cortex
|
|
29
|
+
Cortex speaks GraphQL, and by default it enables the GraphQL playground. If you're just using default options, that's at [http://localhost:4000/graphql](http://localhost:4000/graphql). From there you can begin making requests and test out the pathways (listed under Query) to your heart's content.
|
|
30
|
+
|
|
31
|
+
When it's time to talk to Cortex from an app, that's simple as well - you just use standard GraphQL client conventions:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { useApolloClient, gql } from "@apollo/client"
|
|
35
|
+
|
|
36
|
+
const TRANSLATE = gql`
|
|
37
|
+
query Translate($text: String!, $to: String!) {
|
|
38
|
+
translate(text: $text, to: $to) {
|
|
39
|
+
result
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
apolloClient.query({
|
|
44
|
+
query: TRANSLATE,
|
|
45
|
+
variables: {
|
|
46
|
+
text: inputText,
|
|
47
|
+
to: translationLanguage,
|
|
48
|
+
}
|
|
49
|
+
}).then(e => {
|
|
50
|
+
setTranslatedText(e.data.translate.result.trim())
|
|
51
|
+
}).catch(e => {
|
|
52
|
+
// catch errors
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
## Default Queries (pathways)
|
|
56
|
+
Below are the default pathways provided with Cortex. These can be used as is, overridden, or disabled via configuration. For documentation on each one including input and output parameters, please look at them in the GraphQL Playground.
|
|
57
|
+
- `bias`: Identifies and measures any potential biases in a text
|
|
58
|
+
- `chat`: Enables users to have a conversation with the chatbot
|
|
59
|
+
- `complete`: Autocompletes words or phrases based on user input
|
|
60
|
+
- `edit`: Checks for and suggests corrections for spelling and grammar errors
|
|
61
|
+
- `entities`: Identifies and extracts important entities from text
|
|
62
|
+
- `paraphrase`: Suggests alternative phrasing for text
|
|
63
|
+
- `sentiment`: Analyzes and identifies the overall sentiment or mood of a text
|
|
64
|
+
- `summary`: Condenses long texts or articles into shorter summaries
|
|
65
|
+
- `topics`: Analyzes and identifies the main topic or subject of a text
|
|
66
|
+
- `translate`: Translates text from one language to another
|
|
67
|
+
## Extensibility
|
|
68
|
+
Cortex is designed to be highly extensible. This allows you to customize the API to fit your needs. You can add new features, modify existing features, and even add integrations with other APIs and models.
|
|
69
|
+
## Configuration
|
|
70
|
+
Configuration of Cortex is done via a [convict](https://github.com/mozilla/node-convict/tree/master) object called `config`. The `config` object is built by combining the default values and any values specified in a configuration file or environment variables. The environment variables take precedence over the values in the configuration file. Below are the configurable properties and their defaults:
|
|
71
|
+
|
|
72
|
+
- `basePathwayPath`: The path to the base pathway (the prototype pathway) for Cortex. Default properties for the pathway are set from their values in this basePathway. Default is path.join(__dirname, 'pathways', 'basePathway.js').
|
|
73
|
+
- `corePathwaysPath`: The path to the core pathways for Cortex. Default is path.join(__dirname, 'pathways').
|
|
74
|
+
- `cortexConfigFile`: The path to a JSON configuration file for the project. Default is null. The value can be set using the `CORTEX_CONFIG_FILE` environment variable.
|
|
75
|
+
- `defaultModelName`: The default model name for the project. Default is null. The value can be set using the `DEFAULT_MODEL_NAME` environment variable.
|
|
76
|
+
- `enableCache`: A boolean flag indicating whether to enable caching. Default is true. The value can be set using the `CORTEX_ENABLE_CACHE` environment variable.
|
|
77
|
+
- `models`: An object containing the different models used by the project. The value can be set using the `CORTEX_MODELS` environment variable. Cortex is model and vendor agnostic - you can use this config to set up models of any type from any vendor.
|
|
78
|
+
- `openaiApiKey`: The API key used for accessing the OpenAI API. This is sensitive information and has no default value. The value can be set using the `OPENAI_API_KEY` environment variable.
|
|
79
|
+
- `openaiApiUrl`: The URL used for accessing the OpenAI API. Default is https://api.openai.com/v1/completions. The value can be set using the `OPENAI_API_URL` environment variable.
|
|
80
|
+
- `openaiDefaultModel`: The default model name used for the OpenAI API. Default is text-davinci-003. The value can be set using the `OPENAI_DEFAULT_MODEL` environment variable.
|
|
81
|
+
- `pathways`: An object containing pathways for the project. The default is an empty object that is filled in during the `buildPathways` step.
|
|
82
|
+
- `pathwaysPath`: The path to custom pathways for the project. Default is null.
|
|
83
|
+
- `PORT`: The port number for the Cortex server. Default is 4000. The value can be set using the `CORTEX_PORT` environment variable.
|
|
84
|
+
- `storageConnectionString`: The connection string used for accessing storage. This is sensitive information and has no default value. The value can be set using the `STORAGE_CONNECTION_STRING` environment variable.
|
|
85
|
+
|
|
86
|
+
The `buildPathways` function takes the config object and builds the `pathways` object by loading the core pathways and any custom pathways specified in the `pathwaysPath` property of the config object. The function returns the `pathways` object.
|
|
87
|
+
|
|
88
|
+
The `buildModels` function takes the `config` object and builds the `models` object by compiling handlebars templates for each model specified in the `models` property of the config object. The function returns the `models` object.
|
|
89
|
+
|
|
90
|
+
The `config` object can be used to access configuration values throughout the project. For example, to get the port number for the server, use
|
|
91
|
+
```js
|
|
92
|
+
config.get('PORT')
|
|
93
|
+
```
|
|
94
|
+
## Pathways
|
|
95
|
+
Pathways are a core concept in Cortex. They let users define new functionality and extend the platform. Each pathway is a single JavaScript file that encapsulates the data and logic needed to define a functional API endpoint. Effectively, pathways define how a request from a client is processed when sent to Cortex.
|
|
96
|
+
|
|
97
|
+
To add a new pathway to Cortex, you create a new JavaScript file and define the prompts, properties, and functions that define the function you want to implement. Cortex provides defaults for almost everything, so in the simplest case a pathway can really just consist of a string prompt. You can then save this file in the `pathways` directory in your Cortex project and it will be picked up and made available as a GraphQL query.
|
|
98
|
+
|
|
99
|
+
Example of a very simple pathway (`spelling.js`):
|
|
100
|
+
```js
|
|
101
|
+
module.exports = {
|
|
102
|
+
prompt: `{{text}}\n\nRewrite the above using British English spelling:`
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
### Prompt
|
|
106
|
+
When you define a new pathway, you need to at least specify a prompt that will be passed to the model for processing. In the simplest case, a prompt is really just a string, but the prompt is polymorphic - it can be a string or an object that contains information for the model API that you wish to call. Prompts can also be an array of strings or an array of objects for sequential operations. In this way Cortex aims to support the most simple to advanced prompting scenarios.
|
|
107
|
+
|
|
108
|
+
In the above example, the pathway simply prompts the model to rewrite some text using British English spelling. If you look closely, you'll notice the embedded `{{text}}` parameter. In Cortex, all prompt strings are actually [Handlebars](https://handlebarsjs.com/) templates. So in this case, that parameter will be replaced before prompt execution with the incoming query variable called `text`. You can refer to almost any pathway parameter or system property in the prompt definition and it will be replaced before execution.
|
|
109
|
+
|
|
110
|
+
### Cortex System Properties
|
|
111
|
+
As Cortex executes the prompts in your pathway, it creates and maintains certain system properties that can be injected into prompts via Handlebars templating. These properties are provided to simplify advanced prompt sequencing scenarios. The system properties include:
|
|
112
|
+
- `text`: Always stores the value of the `text` parameter passed into the query. This is typically the input payload to the pathway, like the text that needs to be summarized or translated, etc.
|
|
113
|
+
- `now`: This is actually a Handlebars helper function that will return the current date and time - very useful for injecting temporal context into a prompt.
|
|
114
|
+
- `previousResult`: This stores the value of the previous prompt execution if there is one. `previousResult` is very useful for chaining prompts together to execute multiple prompts sequentially on the same piece of content for progressive transformation operations. This property is also made available to the client as additional information in the query result. Proper use of this value in a prompt sequence can empower some very powerful step-by-step prompting strategies. For example, this three part sequential prompt implements a context-sensitive translation that is significantly better at translating specific people and place names:
|
|
115
|
+
```js
|
|
116
|
+
prompt:
|
|
117
|
+
[
|
|
118
|
+
`{{{text}}}\nCopy the names of all people and places exactly from this document in the language above:\n`,
|
|
119
|
+
`Original Language:\n{{{previousResult}}}\n\n{{to}}:\n`,
|
|
120
|
+
`Entities in the document:\n\n{{{previousResult}}}\n\nDocument:\n{{{text}}}\nRewrite the document in {{to}}. If the document is already in {{to}}, copy it exactly below:\n`
|
|
121
|
+
]
|
|
122
|
+
```
|
|
123
|
+
- `savedContext`: The savedContext property is an object that the pathway can define the properties of. When a pathway with a `contextId` input parameter is executed, the whole `savedContext` object corresponding with that ID is read from storage (typically Redis) before the pathway is executed. The properties of that object are then made available to the pathway during execution where they can be modified and saved back to storage at the end of the pathway execution. Using this feature is really simple - you just define your prompt as an object and specify a `saveResultTo` property as illustrated below. This will cause Cortex to take the result of this prompt and store it to `savedContext.userContext` from which it will then be persisted to storage.
|
|
124
|
+
```js
|
|
125
|
+
new Prompt({ prompt: `User details:\n{{{userContext}}}\n\nExtract all personal details about the user that you can find in either the user details above or the conversation below and list them below.\n\nChat History:\n{{{conversationSummary}}}\n\nChat:\n{{{text}}}\n\nPersonal Details:\n`, saveResultTo: `userContext` }),
|
|
126
|
+
```
|
|
127
|
+
### Input Processing
|
|
128
|
+
A core function of Cortex is dealing with token limited interfaces. To this end, Cortex has built-in strategies for dealing with long input. These strategies are `chunking`, `summarization`, and `truncation`. All are configurable at the pathway level.
|
|
129
|
+
- `useInputChunking`: If true, Cortex will calculate the optimal chunk size from the model max tokens and the size of the prompt and then will split the input `text` into `n` chunks of that size. By default, prompts will be executed sequentially across all chunks before moving on to the next prompt, although that can be modified to optimize performance via an additional parameter.
|
|
130
|
+
- `useParallelChunkProcessing`: If this parameter is true, then sequences of prompts will be executed end to end on each chunk in parallel. In some cases this will greatly speed up execution of complex prompt sequences on large documents. Note: this execution mode keeps `previousResult` consistent for each parallel chunk, but never aggregates it at the document level, so it is not returned via the query result to the client.
|
|
131
|
+
- `truncateFromFront`: If true, when Cortex needs to truncate input, it will choose the first N characters of the input instead of the default which is to take the last N characters.
|
|
132
|
+
- `useInputSummarization`: If true, Cortex will call the `summarize` core pathway on the input `text` before passing it on to the prompts.
|
|
133
|
+
### Output Processing
|
|
134
|
+
Cortex provides built in functions to turn loosely formatted text output from the model API calls into structured objects for return to the application. Specifically, Cortex provides parsers for numbered lists of strings and numbered lists of objects. These are used in pathways like this:
|
|
135
|
+
```js
|
|
136
|
+
module.exports = {
|
|
137
|
+
temperature: 0,
|
|
138
|
+
prompt: `{{text}}\n\nList the top {{count}} entities and their definitions for the above in the format {{format}}:`,
|
|
139
|
+
format: `(name: definition)`,
|
|
140
|
+
inputParameters: {
|
|
141
|
+
count: 5,
|
|
142
|
+
},
|
|
143
|
+
list: true,
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
By simply specifying a `format` property and a `list` property, this pathway invokes a built in parser that will take the result of the prompt and try to parse it into an array of 5 objects. The `list` property can be set with or without a `format` property. If there is no `format`, the list will simply try to parse the string into a list of strings. All of this default behavior is implemented in `parser.js`, and you can override it to do whatever you want by providing your own `parser` function in your pathway.
|
|
147
|
+
## Custom Pathways
|
|
148
|
+
Pathways in Cortex OS are implemented as JavaScript files that export a module. A pathway module is an object that contains properties that define the prompts and behavior of the pathway. Most properties have functional defaults, so you can only implement the bits that are important to you. The main properties of a pathway module are:
|
|
149
|
+
|
|
150
|
+
* `prompt`: The prompt that the pathway uses to interact with the model.
|
|
151
|
+
* `inputParameters`: Any custom parameters to the GraphQL query that the pathway requires to run.
|
|
152
|
+
* `resolver`: The resolver function that processes the input, executes the prompts, and returns the result.
|
|
153
|
+
* `parser`: The parser function that processes the output from the prompts and formats the result for return.
|
|
154
|
+
|
|
155
|
+
### Custom Resolver
|
|
156
|
+
The resolver property defines the function that processes the input and returns the result. The resolver function is an asynchronous function that takes four parameters: `parent`, `args`, `contextValue`, and `info`. The `parent` parameter is the parent object of the resolver function. The `args` parameter is an object that contains the input parameters and any other parameters that are passed to the resolver. The `contextValue` parameter is an object that contains the context and configuration of the pathway. The `info` parameter is an object that contains information about the GraphQL query that triggered the resolver.
|
|
157
|
+
|
|
158
|
+
The core pathway `summary.js` below is implemented using custom pathway logic and a custom resolver to effectively target a specific summary length:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
const { semanticTruncate } = require('../graphql/chunker');
|
|
162
|
+
const { PathwayResolver } = require('../graphql/pathwayResolver');
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
prompt: `{{{text}}}\n\nWrite a summary of the above text:\n\n`,
|
|
166
|
+
|
|
167
|
+
inputParameters: {
|
|
168
|
+
targetLength: 500,
|
|
169
|
+
},
|
|
170
|
+
resolver: async (parent, args, contextValue, info) => {
|
|
171
|
+
const { config, pathway, requestState } = contextValue;
|
|
172
|
+
const originalTargetLength = args.targetLength;
|
|
173
|
+
const errorMargin = 0.2;
|
|
174
|
+
const lowTargetLength = originalTargetLength * (1 - errorMargin);
|
|
175
|
+
const targetWords = Math.round(originalTargetLength / 6.6);
|
|
176
|
+
|
|
177
|
+
// if the text is shorter than the summary length, just return the text
|
|
178
|
+
if (args.text.length <= originalTargetLength) {
|
|
179
|
+
return args.text;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const MAX_ITERATIONS = 5;
|
|
183
|
+
let summary = '';
|
|
184
|
+
let bestSummary = '';
|
|
185
|
+
let pathwayResolver = new PathwayResolver({ config, pathway, requestState });
|
|
186
|
+
// modify the prompt to be words-based instead of characters-based
|
|
187
|
+
pathwayResolver.pathwayPrompt = `{{{text}}}\n\nWrite a summary of the above text in exactly ${targetWords} words:\n\n`
|
|
188
|
+
|
|
189
|
+
let i = 0;
|
|
190
|
+
// reprompt if summary is too long or too short
|
|
191
|
+
while (((summary.length > originalTargetLength) || (summary.length < lowTargetLength)) && i < MAX_ITERATIONS) {
|
|
192
|
+
summary = await pathwayResolver.resolve(args);
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// if the summary is still too long, truncate it
|
|
197
|
+
if (summary.length > originalTargetLength) {
|
|
198
|
+
return semanticTruncate(summary, originalTargetLength);
|
|
199
|
+
} else {
|
|
200
|
+
return summary;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
### Building and Loading Pathways
|
|
206
|
+
Pathways are loaded from modules in the `pathways` directory. The pathways are built and loaded to the `config` object using the `buildPathways` function. The `buildPathways` function loads the base pathway, the core pathways, and any custom pathways. It then creates a new object that contains all the pathways and adds it to the pathways property of the config object. The order of loading means that custom pathways will always override any core pathways that Cortext provides. While pathways are designed to be self-contained, you can override some pathway properties - including whether they're even available at all - in the `pathways` section of the config file.
|
|
207
|
+
|
|
208
|
+
## Troubleshooting
|
|
209
|
+
If you encounter any issues while using Cortex, there are a few things you can do. First, check the Cortex documentation for any common errors and their solutions. If that does not help, you can also open an issue on the Cortex GitHub repository.
|
|
210
|
+
|
|
211
|
+
## Contributing
|
|
212
|
+
If you would like to contribute to Cortex, there are two ways to do so. You can submit issues to the Cortex GitHub repository or submit pull requests with your proposed changes.
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
Cortex is released under the MIT License. See [LICENSE](https://github.com/ALJAZEERAPLUS/cortex/blob/main/LICENSE) for more details.
|
|
216
|
+
|
|
217
|
+
## API Reference
|
|
218
|
+
Detailed documentation on Cortex's API can be found in the /graphql endpoint of your project. Examples of queries and responses can also be found in the Cortex documentation, along with tips for getting the most out of Cortex.
|
|
219
|
+
|
|
220
|
+
## Roadmap
|
|
221
|
+
Cortex is a constantly evolving project, and the following features are coming soon:
|
|
222
|
+
|
|
223
|
+
* Model-specific cache key optimizations to increase hit rate and reduce cache size
|
|
224
|
+
* Structured analytics and reporting on AI API call frequency, cost, cache hit rate, etc.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
package/config.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const convict = require('convict');
|
|
3
|
+
const handlebars = require("handlebars");
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
// Schema for config
|
|
7
|
+
var config = convict({
|
|
8
|
+
pathwaysPath: {
|
|
9
|
+
format: String,
|
|
10
|
+
default: path.join(process.cwd(), '/pathways'),
|
|
11
|
+
env: 'CORTEX_PATHWAYS_PATH'
|
|
12
|
+
},
|
|
13
|
+
corePathwaysPath: {
|
|
14
|
+
format: String,
|
|
15
|
+
default: path.join(__dirname, 'pathways'),
|
|
16
|
+
env: 'CORTEX_CORE_PATHWAYS_PATH'
|
|
17
|
+
},
|
|
18
|
+
basePathwayPath: {
|
|
19
|
+
format: String,
|
|
20
|
+
default: path.join(__dirname, 'pathways', 'basePathway.js'),
|
|
21
|
+
env: 'CORTEX_BASE_PATHWAY_PATH'
|
|
22
|
+
},
|
|
23
|
+
storageConnectionString: {
|
|
24
|
+
doc: 'Connection string used for access to Storage',
|
|
25
|
+
format: '*',
|
|
26
|
+
default: '',
|
|
27
|
+
sensitive: true,
|
|
28
|
+
env: 'STORAGE_CONNECTION_STRING'
|
|
29
|
+
},
|
|
30
|
+
PORT: {
|
|
31
|
+
format: 'port',
|
|
32
|
+
default: 4000,
|
|
33
|
+
env: 'CORTEX_PORT'
|
|
34
|
+
},
|
|
35
|
+
pathways: {
|
|
36
|
+
format: Object,
|
|
37
|
+
default: {}
|
|
38
|
+
},
|
|
39
|
+
enableCache: {
|
|
40
|
+
format: Boolean,
|
|
41
|
+
default: true,
|
|
42
|
+
env: 'CORTEX_ENABLE_CACHE'
|
|
43
|
+
},
|
|
44
|
+
defaultModelName: {
|
|
45
|
+
format: String,
|
|
46
|
+
default: null,
|
|
47
|
+
env: 'DEFAULT_MODEL_NAME'
|
|
48
|
+
},
|
|
49
|
+
models: {
|
|
50
|
+
format: Object,
|
|
51
|
+
default: {
|
|
52
|
+
"oai-td3": {
|
|
53
|
+
"url": "{{openaiApiUrl}}",
|
|
54
|
+
"headers": {
|
|
55
|
+
"Authorization": "Bearer {{openaiApiKey}}",
|
|
56
|
+
"Content-Type": "application/json"
|
|
57
|
+
},
|
|
58
|
+
"params": {
|
|
59
|
+
"model": "{{openaiDefaultModel}}"
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
env: 'CORTEX_MODELS'
|
|
64
|
+
},
|
|
65
|
+
openaiDefaultModel: {
|
|
66
|
+
format: String,
|
|
67
|
+
default: 'text-davinci-003',
|
|
68
|
+
env: 'OPENAI_DEFAULT_MODEL'
|
|
69
|
+
},
|
|
70
|
+
openaiApiKey: {
|
|
71
|
+
format: String,
|
|
72
|
+
default: null,
|
|
73
|
+
env: 'OPENAI_API_KEY',
|
|
74
|
+
sensitive: true
|
|
75
|
+
},
|
|
76
|
+
openaiApiUrl: {
|
|
77
|
+
format: String,
|
|
78
|
+
default: 'https://api.openai.com/v1/completions',
|
|
79
|
+
env: 'OPENAI_API_URL'
|
|
80
|
+
},
|
|
81
|
+
cortexConfigFile: {
|
|
82
|
+
format: String,
|
|
83
|
+
default: null,
|
|
84
|
+
env: 'CORTEX_CONFIG_FILE'
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Read in environment variables and set up service configuration
|
|
89
|
+
const configFile = config.get('cortexConfigFile');
|
|
90
|
+
|
|
91
|
+
// Load config file
|
|
92
|
+
if (configFile && fs.existsSync(configFile)) {
|
|
93
|
+
console.log('Loading config from', configFile);
|
|
94
|
+
config.loadFile(configFile);
|
|
95
|
+
} else {
|
|
96
|
+
const openaiApiKey = config.get('openaiApiKey');
|
|
97
|
+
if (!openaiApiKey) {
|
|
98
|
+
throw console.log('No config file or api key specified. Please set the OPENAI_API_KEY to use OAI or use CORTEX_CONFIG_FILE environment variable to point at the Cortex configuration for your project.');
|
|
99
|
+
}else {
|
|
100
|
+
console.log(`Using default model with OPENAI_API_KEY environment variable`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
// Build and load pathways to config
|
|
106
|
+
const buildPathways = (config) => {
|
|
107
|
+
const { pathwaysPath, corePathwaysPath, basePathwayPath } = config.getProperties();
|
|
108
|
+
|
|
109
|
+
// Load cortex base pathway
|
|
110
|
+
const basePathway = require(basePathwayPath);
|
|
111
|
+
|
|
112
|
+
// Load core pathways, default from the Cortex package
|
|
113
|
+
console.log('Loading core pathways from', corePathwaysPath)
|
|
114
|
+
let loadedPathways = require(corePathwaysPath);
|
|
115
|
+
|
|
116
|
+
// Load custom pathways and override core pathways if same
|
|
117
|
+
if (pathwaysPath && fs.existsSync(pathwaysPath)) {
|
|
118
|
+
console.log('Loading custom pathways from', pathwaysPath)
|
|
119
|
+
const customPathways = require(pathwaysPath);
|
|
120
|
+
loadedPathways = { ...loadedPathways, ...customPathways };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// This is where we integrate pathway overrides from the config
|
|
124
|
+
// file. This can run into a partial definition issue if the
|
|
125
|
+
// config file contains pathways that no longer exist.
|
|
126
|
+
const pathways = config.get('pathways');
|
|
127
|
+
for (const [key, def] of Object.entries(loadedPathways)) {
|
|
128
|
+
const pathway = { ...basePathway, name: key, objName: key.charAt(0).toUpperCase() + key.slice(1), ...def, ...pathways[key] };
|
|
129
|
+
pathways[def.name || key] = pathways[key] = pathway;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add pathways to config
|
|
133
|
+
config.load({ pathways })
|
|
134
|
+
|
|
135
|
+
return pathways;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Build and load models to config
|
|
139
|
+
const buildModels = (config) => {
|
|
140
|
+
const { models } = config.getProperties();
|
|
141
|
+
|
|
142
|
+
for (const [key, model] of Object.entries(models)) {
|
|
143
|
+
// Compile handlebars templates for models
|
|
144
|
+
models[key] = JSON.parse(handlebars.compile(JSON.stringify(model))({ ...config.getEnv(), ...config.getProperties() }))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Add constructed models to config
|
|
148
|
+
config.load({ models });
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
// Check that models are specified, Cortex cannot run without a model
|
|
152
|
+
if (Object.keys(config.get('models')).length <= 0) {
|
|
153
|
+
throw console.log('No models specified! Please set the models in your config file or via CORTEX_MODELS environment variable to point at the models for your project.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Set default model name to the first model in the config in case no default is specified
|
|
157
|
+
if (!config.get('defaultModelName')) {
|
|
158
|
+
console.log('No default model specified, using first model as default.');
|
|
159
|
+
config.load({ defaultModelName: Object.keys(config.get('models'))[0] });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return models;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// TODO: Perform validation
|
|
166
|
+
// config.validate({ allowed: 'strict' });
|
|
167
|
+
|
|
168
|
+
module.exports = { config, buildPathways, buildModels };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const { encode, decode } = require('gpt-3-encoder')
|
|
2
|
+
|
|
3
|
+
const estimateCharPerToken = (text) => {
|
|
4
|
+
// check text only contains asciish characters
|
|
5
|
+
if (/^[ -~\t\n\r]+$/.test(text)) {
|
|
6
|
+
return 4;
|
|
7
|
+
}
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const getLastNChar = (text, maxLen) => {
|
|
12
|
+
if (text.length > maxLen) {
|
|
13
|
+
//slice text to avoid maxLen limit but keep the last n characters up to a \n or space to avoid cutting words
|
|
14
|
+
text = text.slice(-maxLen);
|
|
15
|
+
text = text.slice(text.search(/\s/) + 1);
|
|
16
|
+
}
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const getLastNToken = (text, maxTokenLen) => {
|
|
21
|
+
const encoded = encode(text);
|
|
22
|
+
if (encoded.length > maxTokenLen) {
|
|
23
|
+
text = decode(encoded.slice(-maxTokenLen));
|
|
24
|
+
text = text.slice(text.search(/\s/) + 1); // skip potential partial word
|
|
25
|
+
}
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getFirstNToken = (text, maxTokenLen) => {
|
|
30
|
+
const encoded = encode(text);
|
|
31
|
+
if (encoded.length > maxTokenLen) {
|
|
32
|
+
text = decode(encoded.slice(0, maxTokenLen + 1));
|
|
33
|
+
text = text.slice(0,text.search(/\s[^\s]*$/)); // skip potential partial word
|
|
34
|
+
}
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isBigChunk = ({ text, maxChunkLength, maxChunkToken }) => {
|
|
39
|
+
if (maxChunkLength && text.length > maxChunkLength) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (maxChunkToken && encode(text).length > maxChunkToken) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const getSemanticChunks = ({ text, maxChunkLength, maxChunkToken,
|
|
49
|
+
enableParagraphChunks = true, enableSentenceChunks = true, enableLineChunks = true,
|
|
50
|
+
enableWordChunks = true, finallyMergeChunks = true }) => {
|
|
51
|
+
|
|
52
|
+
if (maxChunkLength && maxChunkLength <= 0) {
|
|
53
|
+
throw new Error(`Invalid maxChunkLength: ${maxChunkLength}`);
|
|
54
|
+
}
|
|
55
|
+
if (maxChunkToken && maxChunkToken <= 0) {
|
|
56
|
+
throw new Error(`Invalid maxChunkToken: ${maxChunkToken}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isBig = (text) => {
|
|
60
|
+
return isBigChunk({ text, maxChunkLength, maxChunkToken });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// split into paragraphs
|
|
64
|
+
let paragraphChunks = enableParagraphChunks ? text.split('\n\n') : [text];
|
|
65
|
+
|
|
66
|
+
// Chunk paragraphs into sentences if needed
|
|
67
|
+
const sentenceChunks = enableSentenceChunks ? [] : paragraphChunks;
|
|
68
|
+
for (let i = 0; enableSentenceChunks && i < paragraphChunks.length; i++) {
|
|
69
|
+
if (isBig(paragraphChunks[i])) { // too long paragraph, chunk into sentences
|
|
70
|
+
sentenceChunks.push(...paragraphChunks[i].split('.\n')); // split into sentences
|
|
71
|
+
} else {
|
|
72
|
+
sentenceChunks.push(paragraphChunks[i]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Chunk sentences with newlines if needed
|
|
77
|
+
const newlineChunks = enableLineChunks ? [] : sentenceChunks;
|
|
78
|
+
for (let i = 0; enableLineChunks && i < sentenceChunks.length; i++) {
|
|
79
|
+
if (isBig(sentenceChunks[i])) { // too long, split into lines
|
|
80
|
+
newlineChunks.push(...sentenceChunks[i].split('\n'));
|
|
81
|
+
} else {
|
|
82
|
+
newlineChunks.push(sentenceChunks[i]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Chunk sentences into word chunks if needed
|
|
87
|
+
let chunks = enableWordChunks ? [] : newlineChunks;
|
|
88
|
+
for (let j = 0; enableWordChunks && j < newlineChunks.length; j++) {
|
|
89
|
+
if (isBig(newlineChunks[j])) { // too long sentence, chunk into words
|
|
90
|
+
const words = newlineChunks[j].split(' ');
|
|
91
|
+
// merge words into chunks up to max
|
|
92
|
+
let chunk = '';
|
|
93
|
+
for (let k = 0; k < words.length; k++) {
|
|
94
|
+
if (isBig( chunk + ' ' + words[k]) ) {
|
|
95
|
+
chunks.push(chunk.trim());
|
|
96
|
+
chunk = '';
|
|
97
|
+
}
|
|
98
|
+
chunk += words[k] + ' ';
|
|
99
|
+
}
|
|
100
|
+
if (chunk.length > 0) {
|
|
101
|
+
chunks.push(chunk.trim());
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
chunks.push(newlineChunks[j]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
chunks = chunks.filter(Boolean).map(chunk => '\n' + chunk + '\n'); //filter empty chunks and add newlines
|
|
109
|
+
|
|
110
|
+
return finallyMergeChunks ? mergeChunks({ chunks, maxChunkLength, maxChunkToken }) : chunks;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const mergeChunks = ({ chunks, maxChunkLength, maxChunkToken }) => {
|
|
114
|
+
const isBig = (text) => {
|
|
115
|
+
return isBigChunk({ text, maxChunkLength, maxChunkToken });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Merge chunks into maxChunkLength chunks
|
|
119
|
+
let mergedChunks = [];
|
|
120
|
+
let chunk = '';
|
|
121
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
122
|
+
if (isBig(chunk + ' ' + chunks[i])) {
|
|
123
|
+
mergedChunks.push(chunk);
|
|
124
|
+
chunk = '';
|
|
125
|
+
}
|
|
126
|
+
chunk += chunks[i];
|
|
127
|
+
}
|
|
128
|
+
if (chunk.length > 0) {
|
|
129
|
+
mergedChunks.push(chunk);
|
|
130
|
+
}
|
|
131
|
+
return mergedChunks;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
const semanticTruncate = (text, maxLength) => {
|
|
136
|
+
if (text.length > maxLength) {
|
|
137
|
+
text = getSemanticChunks({ text, maxChunkLength: maxLength })[0].slice(0, maxLength - 3).trim() + "...";
|
|
138
|
+
}
|
|
139
|
+
return text;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
getSemanticChunks, semanticTruncate, mergeChunks,
|
|
146
|
+
getLastNChar, getLastNToken, getFirstNToken, estimateCharPerToken
|
|
147
|
+
}
|