@aj-archipelago/cortex 1.0.1 → 1.0.2

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/.eslintignore ADDED
@@ -0,0 +1,30 @@
1
+ # Ignore build artifacts
2
+ /dist
3
+ /build
4
+
5
+ # Ignore node_modules
6
+ /node_modules
7
+
8
+ # Ignore log files
9
+ *.log
10
+
11
+ # Ignore any config files
12
+ .env
13
+ .env.*
14
+
15
+ # Ignore coverage reports
16
+ /coverage
17
+
18
+ # Ignore documentation
19
+ /docs
20
+
21
+ # Ignore any generated or bundled files
22
+ *.min.js
23
+ *.bundle.js
24
+
25
+ # Ignore any files generated by your IDE or text editor
26
+ .idea/
27
+ .vscode/
28
+ *.sublime-*
29
+ *.iml
30
+ *.swp
package/.eslintrc ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "env": {
3
+ "browser": true,
4
+ "es2021": true,
5
+ "node": true
6
+ },
7
+ "extends": [
8
+ "eslint:recommended"
9
+ ],
10
+ "parserOptions": {
11
+ "ecmaVersion": 12,
12
+ "sourceType": "module"
13
+ },
14
+ "plugins": [
15
+ "import"
16
+ ],
17
+ "rules": {
18
+ "import/no-unresolved": "error",
19
+ "import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
20
+ "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
21
+ },
22
+ "settings": {
23
+ "import/resolver": {
24
+ "node": {
25
+ "extensions": [".js"],
26
+ "moduleDirectory": ["node_modules", "src"]
27
+ }
28
+ },
29
+ "import/core-modules": ["ava"]
30
+ }
31
+ }
package/README.md CHANGED
@@ -2,6 +2,16 @@
2
2
  Cortex simplifies and accelerates the process of creating applications that harness the power of modern AI models like chatGPT and GPT-4 by providing a structured interface (GraphQL or REST) to a powerful prompt execution environment. This enables complex augmented prompting and abstracts away most of the complexity of managing model connections like chunking input, rate limiting, formatting output, caching, and handling errors.
3
3
  ## Why build Cortex?
4
4
  Modern AI models are transformational, but a number of complexities emerge when developers start using them to deliver application-ready functions. 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.
5
+
6
+ ## Okay, but what can I really do with this thing?
7
+ Yikes. Everything! It's kind of an LLM swiss army knife. Here are some ideas:
8
+ * Create custom chat agents with memory and personalization and then expose it to a bunch of different UIs (custom chat portals, Slack, teams, etc. - anything that can speak to a REST or GraphQL endpoint)
9
+ * Create custom coding assistants (code generation, code reviews, test writing, AI pair programming) and easily integrate them with your existing editing tools.
10
+ * Create powerful AI editing tools (copy editing, paraphrasing, summarization, etc.) tools for your company and then integrate them with your existing workflow tools without having to build all the LLM-handling logic into those tools.
11
+ * Make LLM chains and agents from LangChain.js available via scalable REST or GraphQL endpoints.
12
+ * Put a REST or GraphQL front end on your locally-run models (e.g. llama.cpp) and use them in concert with other tools.
13
+ * The sky is the limit!
14
+
5
15
  ## Features
6
16
 
7
17
  * Simple architecture to build custom functional endpoints (called `pathways`), that implement common NL AI tasks. Default pathways include chat, summarization, translation, paraphrasing, completion, spelling and grammar correction, entity extraction, sentiment analysis, and bias analysis.
@@ -0,0 +1,70 @@
1
+ {
2
+ "defaultModelName": "oai-td3",
3
+ "models": {
4
+ "azure-translate": {
5
+ "type": "AZURE-TRANSLATE",
6
+ "url": "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0",
7
+ "headers": {
8
+ "Ocp-Apim-Subscription-Key": "{{ARCHIPELAGO_TRANSLATE_KEY}}",
9
+ "Ocp-Apim-Subscription-Region": "eastus",
10
+ "Content-Type": "application/json"
11
+ },
12
+ "requestsPerSecond": 10,
13
+ "maxTokenLength": 2000
14
+ },
15
+ "oai-td3": {
16
+ "type": "OPENAI-COMPLETION",
17
+ "url": "https://api.openai.com/v1/completions",
18
+ "headers": {
19
+ "Authorization": "Bearer {{OPENAI_API_KEY}}",
20
+ "Content-Type": "application/json"
21
+ },
22
+ "params": {
23
+ "model": "text-davinci-003"
24
+ },
25
+ "requestsPerSecond": 10,
26
+ "maxTokenLength": 4096
27
+ },
28
+ "oai-gpturbo": {
29
+ "type": "OPENAI-CHAT",
30
+ "url": "https://api.openai.com/v1/chat/completions",
31
+ "headers": {
32
+ "Authorization": "Bearer {{OPENAI_API_KEY}}",
33
+ "Content-Type": "application/json"
34
+ },
35
+ "params": {
36
+ "model": "gpt-3.5-turbo"
37
+ },
38
+ "requestsPerSecond": 10,
39
+ "maxTokenLength": 8192
40
+ },
41
+ "oai-gpt4": {
42
+ "type": "OPENAI-CHAT",
43
+ "url": "https://api.openai.com/v1/chat/completions",
44
+ "headers": {
45
+ "Authorization": "Bearer {{OPENAI_API_KEY}}",
46
+ "Content-Type": "application/json"
47
+ },
48
+ "params": {
49
+ "model": "gpt-4"
50
+ },
51
+ "requestsPerSecond": 10,
52
+ "maxTokenLength": 8192
53
+ },
54
+ "local-llama13B": {
55
+ "type": "LOCAL-CPP-MODEL",
56
+ "executablePath": "../llm/llama.cpp/main",
57
+ "args": [
58
+ "-m", "../llm/llama.cpp/models/13B/ggml-model-q4_0.bin",
59
+ "--repeat_penalty", "1.0",
60
+ "--keep", "0",
61
+ "-t", "8",
62
+ "--mlock"
63
+ ],
64
+ "requestsPerSecond": 10,
65
+ "maxTokenLength": 1024
66
+ }
67
+ },
68
+ "enableCache": false,
69
+ "enableRestEndpoints": false
70
+ }
package/config.js CHANGED
@@ -110,12 +110,6 @@ var config = convict({
110
110
  default: null,
111
111
  env: 'CORTEX_CONFIG_FILE'
112
112
  },
113
- serpApiKey: {
114
- format: String,
115
- default: null,
116
- env: 'SERPAPI_API_KEY',
117
- sensitive: true
118
- },
119
113
  });
120
114
 
121
115
  // Read in environment variables and set up service configuration
@@ -43,7 +43,7 @@ const getSemanticChunks = (text, chunkSize) => {
43
43
  };
44
44
 
45
45
  const breakByParagraphs = (str) => breakByRegex(str, /[\r\n]+/, true);
46
- const breakBySentences = (str) => breakByRegex(str, /(?<=[.。؟!\?!\n])\s+/, true);
46
+ const breakBySentences = (str) => breakByRegex(str, /(?<=[.。؟!?!\n])\s+/, true);
47
47
  const breakByWords = (str) => breakByRegex(str, /(\s,;:.+)/);
48
48
 
49
49
  const createChunks = (tokens) => {
@@ -48,7 +48,7 @@ const buildRestEndpoints = (pathways, app, server, config) => {
48
48
 
49
49
  app.post(`/rest/${name}`, async (req, res) => {
50
50
  const variables = fieldVariableDefs.reduce((acc, variableDef) => {
51
- if (req.body.hasOwnProperty(variableDef.name)) {
51
+ if (Object.prototype.hasOwnProperty.call(req.body, variableDef.name)) {
52
52
  acc[variableDef.name] = req.body[variableDef.name];
53
53
  }
54
54
  return acc;
package/graphql/parser.js CHANGED
@@ -6,6 +6,7 @@ const regexParser = (text, regex) => {
6
6
  // parse numbered list text format into list
7
7
  // this supports most common numbered list returns like "1.", "1)", "1-"
8
8
  const parseNumberedList = (str) => {
9
+ // eslint-disable-next-line no-useless-escape
9
10
  return regexParser(str, /^\s*[\[\{\(]*\d+[\s.=\-:,;\]\)\}]/gm);
10
11
  }
11
12
 
@@ -1,8 +1,9 @@
1
1
  // PathwayPrompter.js
2
- import OpenAIChatPlugin from './plugins/openAIChatPlugin.js';
3
- import OpenAICompletionPlugin from './plugins/openAICompletionPlugin.js';
2
+ import OpenAIChatPlugin from './plugins/openAiChatPlugin.js';
3
+ import OpenAICompletionPlugin from './plugins/openAiCompletionPlugin.js';
4
4
  import AzureTranslatePlugin from './plugins/azureTranslatePlugin.js';
5
5
  import OpenAIWhisperPlugin from './plugins/openAiWhisperPlugin.js';
6
+ import LocalModelPlugin from './plugins/localModelPlugin.js';
6
7
  import handlebars from 'handlebars';
7
8
 
8
9
  // register functions that can be called directly in the prompt markdown
@@ -44,6 +45,9 @@ class PathwayPrompter {
44
45
  case 'OPENAI_WHISPER':
45
46
  plugin = new OpenAIWhisperPlugin(config, pathway);
46
47
  break;
48
+ case 'LOCAL-CPP-MODEL':
49
+ plugin = new LocalModelPlugin(config, pathway);
50
+ break;
47
51
  default:
48
52
  throw new handlebars.Exception(`Unsupported model type: ${model.type}`);
49
53
  }
@@ -8,8 +8,6 @@ import { Prompt } from './prompt.js';
8
8
  import { getv, setv } from '../lib/keyValueStorageClient.js';
9
9
  import { requestState } from './requestState.js';
10
10
 
11
- const MAX_PREVIOUS_RESULT_TOKEN_LENGTH = 1000;
12
-
13
11
  const callPathway = async (config, pathwayName, args, requestState, { text, ...parameters }) => {
14
12
  const pathwayResolver = new PathwayResolver({ config, pathway: config.get(`pathways.${pathwayName}`), args, requestState });
15
13
  return await pathwayResolver.resolve({ text, ...parameters });
@@ -1,20 +1,69 @@
1
1
  // localModelPlugin.js
2
2
  import ModelPlugin from './modelPlugin.js';
3
3
  import { execFileSync } from 'child_process';
4
+ import { encode } from 'gpt-3-encoder';
4
5
 
5
6
  class LocalModelPlugin extends ModelPlugin {
6
7
  constructor(config, pathway) {
7
8
  super(config, pathway);
8
9
  }
9
10
 
10
- async execute(text, parameters, prompt, pathwayResolver) {
11
- const { modelPromptText } = this.getCompiledPrompt(text, parameters, prompt);
11
+ // if the input starts with a chatML response, just return that
12
+ filterFirstResponse(inputString) {
13
+ const regex = /^(.*?)(?=\n<\|im_end\|>|$)/;
14
+ const match = inputString.match(regex);
15
+
16
+ if (match) {
17
+ const firstAssistantResponse = match[1];
18
+ return firstAssistantResponse;
19
+ } else {
20
+ return inputString;
21
+ }
22
+ }
23
+
24
+ getRequestParameters(text, parameters, prompt) {
25
+ let { modelPromptMessages, modelPromptText, tokenLength } = this.getCompiledPrompt(text, parameters, prompt);
26
+ const modelTargetTokenLength = this.getModelMaxTokenLength() * this.getPromptTokenRatio();
27
+
28
+ if (modelPromptMessages) {
29
+ const minMsg = [{ role: "system", content: "" }];
30
+ const addAssistantTokens = encode(this.messagesToChatML(minMsg, true).replace(this.messagesToChatML(minMsg, false), '')).length;
31
+ const requestMessages = this.truncateMessagesToTargetLength(modelPromptMessages, (modelTargetTokenLength - addAssistantTokens));
32
+ modelPromptText = this.messagesToChatML(requestMessages);
33
+ tokenLength = encode(modelPromptText).length;
34
+ }
35
+
36
+ if (tokenLength > modelTargetTokenLength) {
37
+ throw new Error(`Input is too long at ${tokenLength} tokens. The target token length for this pathway is ${modelTargetTokenLength} tokens because the response is expected to take up the rest of the ${this.getModelMaxTokenLength()} tokens that the model can handle. You must reduce the size of the prompt to continue.`);
38
+ }
39
+
40
+ const max_tokens = this.getModelMaxTokenLength() - tokenLength;
41
+
42
+ return {
43
+ prompt: modelPromptText,
44
+ max_tokens: max_tokens,
45
+ temperature: this.temperature ?? 0.7,
46
+ };
47
+ }
48
+
49
+ async execute(text, parameters, prompt, _pathwayResolver) {
50
+ const requestParameters = this.getRequestParameters(text, parameters, prompt);
51
+ const { executablePath, args } = this.model;
52
+ args.push("--prompt", requestParameters.prompt);
53
+ //args.push("--max-tokens", requestParameters.max_tokens);
54
+ //args.push("--temperature", requestParameters.temperature);
12
55
 
13
56
  try {
14
- const result = execFileSync(executablePath, [text], { encoding: 'utf8' });
15
- return result;
57
+ console.log(`\x1b[36mRunning local model:\x1b[0m`, executablePath, args);
58
+ const result = execFileSync(executablePath, args, { encoding: 'utf8' });
59
+ // Remove only the first occurrence of requestParameters.prompt from the result
60
+ // Could have used regex here but then would need to escape the prompt
61
+ const parts = result.split(requestParameters.prompt, 2);
62
+ const modifiedResult = parts[0] + parts[1];
63
+ console.log(`\x1b[36mResult:\x1b[0m`, modifiedResult);
64
+ return this.filterFirstResponse(modifiedResult);
16
65
  } catch (error) {
17
- console.error('Error running local model:', error);
66
+ console.error(`\x1b[31mError running local model:\x1b[0m`, error);
18
67
  throw error;
19
68
  }
20
69
  }
@@ -40,7 +40,7 @@ class ModelPlugin {
40
40
  this.shouldCache = config.get('enableCache') && (pathway.enableCache || pathway.temperature == 0);
41
41
  }
42
42
 
43
- truncateMessagesToTargetLength = (messages, targetTokenLength) => {
43
+ truncateMessagesToTargetLength(messages, targetTokenLength) {
44
44
  // Calculate the token length of each message
45
45
  const tokenLengths = messages.map((message) => ({
46
46
  message,
@@ -97,7 +97,7 @@ class ModelPlugin {
97
97
 
98
98
  // Return the modified messages array
99
99
  return tokenLengths.map(({ message }) => message);
100
- };
100
+ }
101
101
 
102
102
  //convert a messages array to a simple chatML format
103
103
  messagesToChatML(messages, addAssistant = true) {
package/graphql/prompt.js CHANGED
@@ -26,6 +26,7 @@ function promptContains(variable, prompt) {
26
26
  // if it's an array, it's the messages format
27
27
  if (Array.isArray(prompt)) {
28
28
  prompt.forEach(p => {
29
+ // eslint-disable-next-line no-cond-assign
29
30
  while (match = p.content && regexp.exec(p.content)) {
30
31
  matches.push(match[1]);
31
32
  }
@@ -26,12 +26,12 @@ const rootResolver = async (parent, args, contextValue, info) => {
26
26
  }
27
27
 
28
28
  // This resolver is used by the root resolver to process the request
29
- const resolver = async (parent, args, contextValue, info) => {
29
+ const resolver = async (parent, args, contextValue, _info) => {
30
30
  const { pathwayResolver } = contextValue;
31
31
  return await pathwayResolver.resolve(args);
32
32
  }
33
33
 
34
- const cancelRequestResolver = (parent, args, contextValue, info) => {
34
+ const cancelRequestResolver = (parent, args, contextValue, _info) => {
35
35
  const { requestId } = args;
36
36
  const { requestState } = contextValue;
37
37
  requestState[requestId] = { canceled: true };
@@ -10,7 +10,7 @@ import { requestState } from './requestState.js';
10
10
  const subscriptions = {
11
11
  requestProgress: {
12
12
  subscribe: withFilter(
13
- (_, args, __, info) => {
13
+ (_, args, __, _info) => {
14
14
  const { requestIds } = args;
15
15
  for (const requestId of requestIds) {
16
16
  if (!requestState[requestId]) {
@@ -42,10 +42,6 @@ const generateUniqueFolderName = () => {
42
42
  return uniqueOutputPath;
43
43
  }
44
44
 
45
- const generateUniqueTempFileName = () => {
46
- return path.join(os.tmpdir(), uuidv4());
47
- }
48
-
49
45
  async function splitMediaFile(inputPath, chunkDurationInSeconds = 600) {
50
46
  try {
51
47
  const metadata = await ffmpegProbe(inputPath);
@@ -146,15 +142,6 @@ const processYoutubeUrl = async (url) => {
146
142
  return outputFileName;
147
143
  }
148
144
 
149
- function deleteFile(filePath) {
150
- try {
151
- fs.unlinkSync(filePath);
152
- console.log(`File ${filePath} cleaned successfully.`);
153
- } catch (error) {
154
- console.error(`Error deleting file ${filePath}:`, error);
155
- }
156
- }
157
-
158
145
  export {
159
146
  splitMediaFile, deleteTempPath, processYoutubeUrl, isValidYoutubeUrl
160
147
  };
package/lib/request.js CHANGED
@@ -64,7 +64,6 @@ const postWithMonitor = async (model, url, data, axiosConfigObj) => {
64
64
 
65
65
  const MAX_RETRY = 10;
66
66
  const postRequest = async ({ url, data, params, headers, cache }, model) => {
67
- let retry = 0;
68
67
  const errors = []
69
68
  for (let i = 0; i < MAX_RETRY; i++) {
70
69
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,6 +42,7 @@
42
42
  "compromise": "^14.8.1",
43
43
  "compromise-paragraphs": "^0.1.0",
44
44
  "convict": "^6.2.3",
45
+ "express": "^4.18.2",
45
46
  "fluent-ffmpeg": "^2.1.2",
46
47
  "form-data": "^4.0.0",
47
48
  "gpt-3-encoder": "^1.1.4",
@@ -51,12 +52,15 @@
51
52
  "handlebars": "^4.7.7",
52
53
  "keyv": "^4.5.2",
53
54
  "langchain": "^0.0.47",
55
+ "uuid": "^9.0.0",
54
56
  "ws": "^8.12.0",
55
57
  "ytdl-core": "^4.11.2"
56
58
  },
57
59
  "devDependencies": {
58
60
  "ava": "^5.2.0",
59
- "dotenv": "^16.0.3"
61
+ "dotenv": "^16.0.3",
62
+ "eslint": "^8.38.0",
63
+ "eslint-plugin-import": "^2.27.5"
60
64
  },
61
65
  "publishConfig": {
62
66
  "access": "restricted"
@@ -3,20 +3,24 @@
3
3
 
4
4
  // Import required modules
5
5
  import { OpenAI } from "langchain/llms";
6
- import { PromptTemplate } from "langchain/prompts";
7
- import { LLMChain, ConversationChain } from "langchain/chains";
6
+ //import { PromptTemplate } from "langchain/prompts";
7
+ //import { LLMChain, ConversationChain } from "langchain/chains";
8
8
  import { initializeAgentExecutor } from "langchain/agents";
9
9
  import { SerpAPI, Calculator } from "langchain/tools";
10
- import { BufferMemory } from "langchain/memory";
10
+ //import { BufferMemory } from "langchain/memory";
11
11
 
12
12
  export default {
13
13
 
14
14
  // Agent test case
15
- resolver: async (parent, args, contextValue, info) => {
15
+ resolver: async (parent, args, contextValue, _info) => {
16
16
 
17
17
  const { config } = contextValue;
18
+ const env = config.getEnv();
19
+
20
+ // example of reading from a predefined config variable
18
21
  const openAIApiKey = config.get('openaiApiKey');
19
- const serpApiKey = config.get('serpApiKey');
22
+ // example of reading straight from environment
23
+ const serpApiKey = env.SERPAPI_API_KEY;
20
24
 
21
25
  const model = new OpenAI({ openAIApiKey: openAIApiKey, temperature: 0 });
22
26
  const tools = [new SerpAPI( serpApiKey ), new Calculator()];
@@ -16,7 +16,7 @@ export default {
16
16
  },
17
17
 
18
18
  // Custom resolver to generate summaries by reprompting if they are too long or too short.
19
- resolver: async (parent, args, contextValue, info) => {
19
+ resolver: async (parent, args, contextValue, _info) => {
20
20
  const { config, pathway, requestState } = contextValue;
21
21
  const originalTargetLength = args.targetLength;
22
22