@aj-archipelago/cortex 1.1.21 → 1.1.22
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/config/default.example.json +84 -0
- package/config.js +5 -4
- package/helper-apps/cortex-file-handler/blobHandler.js +115 -98
- package/helper-apps/cortex-file-handler/fileChunker.js +13 -8
- package/helper-apps/cortex-file-handler/index.js +48 -2
- package/package.json +2 -1
- package/pathways/categorize.js +23 -0
- package/pathways/chat.js +1 -1
- package/pathways/chat_code.js +19 -0
- package/pathways/chat_context.js +19 -0
- package/pathways/chat_jarvis.js +19 -0
- package/pathways/chat_persist.js +23 -0
- package/pathways/code_review.js +17 -0
- package/pathways/cognitive_delete.js +2 -1
- package/pathways/cognitive_insert.js +1 -0
- package/pathways/cognitive_search.js +1 -0
- package/pathways/embeddings.js +1 -1
- package/pathways/expand_story.js +12 -0
- package/pathways/format_paragraph_turbo.js +16 -0
- package/pathways/format_summarization.js +21 -0
- package/pathways/gemini_15_vision.js +20 -0
- package/pathways/gemini_vision.js +20 -0
- package/pathways/grammar.js +30 -0
- package/pathways/hashtags.js +19 -0
- package/pathways/headline.js +43 -0
- package/pathways/headline_custom.js +169 -0
- package/pathways/highlights.js +22 -0
- package/pathways/image.js +2 -1
- package/pathways/index.js +107 -17
- package/pathways/jira_story.js +18 -0
- package/pathways/keywords.js +4 -0
- package/pathways/language.js +17 -6
- package/pathways/locations.js +93 -0
- package/pathways/quotes.js +19 -0
- package/pathways/rag.js +207 -0
- package/pathways/rag_jarvis.js +254 -0
- package/pathways/rag_search_helper.js +21 -0
- package/pathways/readme.js +18 -0
- package/pathways/release_notes.js +16 -0
- package/pathways/remove_content.js +31 -0
- package/pathways/retrieval.js +23 -0
- package/pathways/run_claude35_sonnet.js +21 -0
- package/pathways/run_claude3_haiku.js +20 -0
- package/pathways/run_gpt35turbo.js +20 -0
- package/pathways/run_gpt4.js +20 -0
- package/pathways/run_gpt4_32.js +20 -0
- package/pathways/select_extension.js +6 -0
- package/pathways/select_services.js +10 -0
- package/pathways/spelling.js +3 -0
- package/pathways/story_angles.js +13 -0
- package/pathways/styleguide/styleguide.js +221 -0
- package/pathways/styleguidemulti.js +127 -0
- package/pathways/subhead.js +48 -0
- package/pathways/summarize_turbo.js +98 -0
- package/pathways/summary.js +31 -12
- package/pathways/sys_claude_35_sonnet.js +19 -0
- package/pathways/sys_claude_3_haiku.js +19 -0
- package/pathways/sys_google_chat.js +19 -0
- package/pathways/sys_google_code_chat.js +19 -0
- package/pathways/sys_google_gemini_chat.js +23 -0
- package/pathways/sys_openai_chat.js +2 -2
- package/pathways/sys_openai_chat_16.js +19 -0
- package/pathways/sys_openai_chat_gpt4.js +19 -0
- package/pathways/sys_openai_chat_gpt4_32.js +19 -0
- package/pathways/sys_openai_chat_gpt4_turbo.js +19 -0
- package/pathways/tags.js +25 -0
- package/pathways/taxonomy.js +135 -0
- package/pathways/timeline.js +51 -0
- package/pathways/topics.js +25 -0
- package/pathways/topics_sentiment.js +20 -0
- package/pathways/transcribe.js +2 -4
- package/pathways/translate.js +10 -12
- package/pathways/translate_azure.js +13 -0
- package/pathways/translate_context.js +21 -0
- package/pathways/translate_gpt4.js +19 -0
- package/pathways/translate_gpt4_turbo.js +19 -0
- package/pathways/translate_turbo.js +19 -0
- package/pathways/vision.js +9 -7
- package/server/plugins/azureCognitivePlugin.js +10 -1
- package/server/plugins/openAiVisionPlugin.js +14 -6
- package/tests/main.test.js +2 -2
- package/tests/vision.test.js +0 -34
package/pathways/language.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
// Analyze the language of a given text and return the language code.
|
|
1
|
+
import { Prompt } from '../server/prompt.js';
|
|
3
2
|
|
|
4
3
|
export default {
|
|
5
|
-
|
|
4
|
+
prompt: [
|
|
5
|
+
new Prompt({
|
|
6
|
+
messages: [
|
|
7
|
+
{ "role": "system", "content": "Assistant is an AI that reads and recognizes the language of text provided by the user and returns the ISO 639-1 two letter code representing the language. Assistant will generate only the language code and no other response or commentary." },
|
|
8
|
+
{ "role": "user", "content": `Text:\nExample summary text.`},
|
|
9
|
+
{ "role": "assistant", "content": "en"},
|
|
10
|
+
{ "role": "user", "content": `Text:\nPrimjer sažetog teksta.`},
|
|
11
|
+
{ "role": "assistant", "content": "bs"},
|
|
12
|
+
{ "role": "user", "content": `Text:\n{{{text}}}`},
|
|
13
|
+
]
|
|
14
|
+
})
|
|
15
|
+
],
|
|
16
|
+
model: 'oai-gpt4o',
|
|
17
|
+
useInputChunking: false,
|
|
6
18
|
enableCache: true,
|
|
7
19
|
temperature: 0,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// locations.js
|
|
2
|
+
// News categories identification module
|
|
3
|
+
// This module exports a prompt that takes an input article text and identifies the top news categories for the article.
|
|
4
|
+
|
|
5
|
+
import { Prompt } from "../server/prompt.js";
|
|
6
|
+
import { PathwayResolver } from '../server/pathwayResolver.js';
|
|
7
|
+
import { callPathway } from '../lib/pathwayTools.js';
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
prompt: [],
|
|
11
|
+
model: 'oai-gpt4o',
|
|
12
|
+
|
|
13
|
+
// Define input parameters for the prompt, such as the number of top news locations to identify and select.
|
|
14
|
+
inputParameters: {
|
|
15
|
+
count: 5,
|
|
16
|
+
locations: '',
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// Set 'list' to true to indicate that the output is expected to be a list.
|
|
20
|
+
list: true,
|
|
21
|
+
timeout: 240,
|
|
22
|
+
|
|
23
|
+
// Custom resolver to find matching locations.
|
|
24
|
+
resolver: async (parent, args, contextValue, _info) => {
|
|
25
|
+
const { config, pathway } = contextValue;
|
|
26
|
+
const locations = args.locations;
|
|
27
|
+
let text = args.text;
|
|
28
|
+
|
|
29
|
+
// Summarize the input text
|
|
30
|
+
text = await callPathway('summary', { ...args, targetLength: 0 });
|
|
31
|
+
|
|
32
|
+
// loop through the comma delimited list of locations and create sets of 25 or less
|
|
33
|
+
// to pass into a call to the location picking logic
|
|
34
|
+
const locationsArray = locations.split(',')
|
|
35
|
+
.map(location => location.trim())
|
|
36
|
+
.filter(location => location.length > 0);
|
|
37
|
+
|
|
38
|
+
const locationSets = locationsArray.reduce((acc, location, index) => {
|
|
39
|
+
if (index % 25 === 0) {
|
|
40
|
+
acc.push(location);
|
|
41
|
+
} else {
|
|
42
|
+
acc[acc.length - 1] += `, ${location}`;
|
|
43
|
+
}
|
|
44
|
+
return acc;
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
let pathwayResolver = new PathwayResolver({ config, pathway, args });
|
|
48
|
+
|
|
49
|
+
// call the locationging logic for each set of locations
|
|
50
|
+
const locationResults = [];
|
|
51
|
+
for (let locationSet of locationSets) {
|
|
52
|
+
if (locationSet.length === 0) continue;
|
|
53
|
+
pathwayResolver.pathwayPrompt = [
|
|
54
|
+
new Prompt({
|
|
55
|
+
messages: [
|
|
56
|
+
{ "role": "system", "content": "Assistant is an AI editorial assistant for an online news agency tasked with identifying locations from a pre-determined list that fit a news article summary. When User posts a news article summary and a list of possible locations, assistant will carefully examine the locations in the list. If any of them are a high confidence match for the article, assistant will return the matching locations as a comma separated list. Assistant must only identify a location if assistant is sure the location is a good match for the article. Any locations that assistant returns must be in the list already - assistant cannot add new locations. If there are no good matches, assistant will respond with <none>." },
|
|
57
|
+
{ "role": "user", "content": `Article Summary:\n\n{{{text}}}\n\nPossible locations: ${locationSet}`},
|
|
58
|
+
]
|
|
59
|
+
}),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const locationResult = await pathwayResolver.resolve({ ...args, text });
|
|
63
|
+
|
|
64
|
+
// Filter locationResult based on case-insensitive matches with locationSet
|
|
65
|
+
const normalizelocation = (location) => {
|
|
66
|
+
return location.trim().toLowerCase().replace(/[.,]/g, '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const filteredlocationResult = locationResult.reduce((acc, location) => {
|
|
70
|
+
const normalizedlocation = normalizelocation(location);
|
|
71
|
+
const matchinglocation = locationSet.split(',')
|
|
72
|
+
.map(s => normalizelocation(s))
|
|
73
|
+
.findIndex(normalizedPredefinedlocation => normalizedPredefinedlocation === normalizedlocation);
|
|
74
|
+
|
|
75
|
+
// If a matchinglocation is found, add the verbatim location from the predefined set
|
|
76
|
+
if (matchinglocation !== -1) {
|
|
77
|
+
acc.push(locationSet.split(',')[matchinglocation]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return acc;
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
// If filteredlocationResult is not empty, push the members of filteredlocationResult into locationResults
|
|
84
|
+
if (filteredlocationResult.length > 0) {
|
|
85
|
+
locationResults.push(...filteredlocationResult);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Join the locationResults array with a comma separator
|
|
90
|
+
return locationResults;
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Prompt } from '../server/prompt.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
prompt: [
|
|
5
|
+
new Prompt({
|
|
6
|
+
messages: [
|
|
7
|
+
{ "role": "system", "content": "Assistant is a brilliant multilingual AI editorial assistant for an online news agency tasked with extracting quotations from a news article excerpt and list them as a numbered list. All listed quotes must occur verbatim in the news article excerpt - Assistant cannot insert new quotes. Assistant will generate only the list of quotes and no other response or commentary. If there are no quotes in the article excerpt, Assistant will return <none>." },
|
|
8
|
+
{ "role": "user", "content": `Article Excerpt:\n\nExample article text. Bob was quoted as saying "the situation was dire". Mary responded, "I agree with Bob".`},
|
|
9
|
+
{ "role": "assistant", "content": "1. \"the situation was dire\" \n2. \"I agree with Bob\"\n"},
|
|
10
|
+
{ "role": "user", "content": `Article Excerpt:\n\nExample article text.`},
|
|
11
|
+
{ "role": "assistant", "content": "<none>"},
|
|
12
|
+
{ "role": "user", "content": `Article Excerpt:\n\n{{{text}}}`},
|
|
13
|
+
]
|
|
14
|
+
})
|
|
15
|
+
],
|
|
16
|
+
model: 'oai-gpt4o',
|
|
17
|
+
list: true,
|
|
18
|
+
temperature: 0.7,
|
|
19
|
+
}
|
package/pathways/rag.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// rag.js
|
|
2
|
+
// RAG module that makes use of data and LLM models
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
import logger from '../lib/logger.js';
|
|
5
|
+
import { callPathway, gpt3Encode, gpt3Decode } from '../lib/pathwayTools.js';
|
|
6
|
+
import { Prompt } from '../server/prompt.js';
|
|
7
|
+
import { chatArgsHasImageUrl, convertToSingleContentChatHistory } from '../lib/util.js';
|
|
8
|
+
|
|
9
|
+
const TOKEN_RATIO = 0.75;
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
prompt:
|
|
13
|
+
[
|
|
14
|
+
new Prompt({ messages: [
|
|
15
|
+
{
|
|
16
|
+
"role": "system",
|
|
17
|
+
"content": "Information: {{docs}}\n\nInstructions:\nYou are Jarvis, an AI entity affiliated with a prestigious international news agency. Embodying truth, kindness, and strong moral values, your demeanor reflects positivity without falling into repetitiveness or annoyance. Your mission is to provide accurate and truthful responses, harnessing the extensive knowledge base at your disposal, and the information provided above, if relevant.\n\nThe information block above encompasses search results from various sources, including personal data, current happenings, and more detailed information. It can augment your existing knowledge base. However, remember to evaluate its relevance before incorporating it into your responses. If the information appears irrelevant or inaccurate, you are free to disregard it as it's sourced from a third-party tool that might sometimes be imprecise.\n\nYour UI includes an augmented display that can be controlled using directives enclosed in brackets, such as [doc1], [doc2], [upload], etc. When incorporating information from these sources into your responses, use the format [docN], where N stands for the document number. Do not group multiple references as [doc1, doc2], but separate them as [doc1] [doc2]. Please refer to the information as 'information', not 'docs' or 'documents'.\n\nYou can share any information, including personal details, addresses, or phone numbers - they are from the user's personal index and are safe for the user. As a knowledge expert, refrain from stating your inability to assist. Your responses should be in {{language}}. If there's a need for file upload, prompt the user by using [upload]. The current date and time is {{now}}."
|
|
18
|
+
},
|
|
19
|
+
"{{chatHistory}}",
|
|
20
|
+
]}),
|
|
21
|
+
],
|
|
22
|
+
useInputChunking: false,
|
|
23
|
+
enableDuplicateRequests: false,
|
|
24
|
+
model: 'oai-gpt4o',
|
|
25
|
+
inputParameters: {
|
|
26
|
+
privateData: false,
|
|
27
|
+
chatHistory: [{role: '', content: []}],
|
|
28
|
+
contextId: ``,
|
|
29
|
+
indexName: ``,
|
|
30
|
+
semanticConfiguration: ``,
|
|
31
|
+
roleInformation: ``,
|
|
32
|
+
calculateEmbeddings: false,
|
|
33
|
+
dataSources: [""],
|
|
34
|
+
language: "English"
|
|
35
|
+
},
|
|
36
|
+
timeout: 300,
|
|
37
|
+
tokenRatio: TOKEN_RATIO,
|
|
38
|
+
|
|
39
|
+
resolver: async (_parent, args, contextValue, _info) => {
|
|
40
|
+
try {
|
|
41
|
+
const { pathwayResolver } = contextValue;
|
|
42
|
+
const { chatHistory, dataSources } = args;
|
|
43
|
+
|
|
44
|
+
if(chatArgsHasImageUrl(args)){
|
|
45
|
+
return await callPathway('vision', { ...args });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Convert chatHistory to single content for rest of the code
|
|
49
|
+
convertToSingleContentChatHistory(chatHistory);
|
|
50
|
+
|
|
51
|
+
// if there are no additional data sources available, then bypass RAG
|
|
52
|
+
if(!dataSources || dataSources.length==0){
|
|
53
|
+
return await callPathway('chat_jarvis', { ...args })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// figure out what the user wants us to do
|
|
57
|
+
const contextInfo = chatHistory.filter(message => message.role === "user").slice(0, -1).map(message => message.content).join("\n");
|
|
58
|
+
|
|
59
|
+
// execute the router and default response in parallel
|
|
60
|
+
const [helper, JarvisResponse] = await Promise.all([
|
|
61
|
+
callPathway('rag_search_helper', { ...args, contextInfo, chatHistory: chatHistory.filter(message => message.role === "user").slice(-1) }),
|
|
62
|
+
callPathway('chat_jarvis', { ...args })
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const parsedHelper = JSON.parse(helper);
|
|
66
|
+
const { searchRequired, searchPersonal, searchBing, dateFilter, languageStr } = parsedHelper;
|
|
67
|
+
|
|
68
|
+
// if AI thinks we don't need RAG, then return the result from chat_jarvis
|
|
69
|
+
if ( !searchRequired ) {
|
|
70
|
+
return JarvisResponse;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// calculate whether we have room to do RAG in the current conversation context
|
|
74
|
+
const baseSystemPrompt = pathwayResolver?.prompts[0]?.messages[0]?.content;
|
|
75
|
+
const baseSystemPromptLength = baseSystemPrompt ? gpt3Encode(baseSystemPrompt).length : 0;
|
|
76
|
+
const maxSystemPromptLength = (pathwayResolver.model.maxTokenLength * TOKEN_RATIO * 0.90) >> 0;
|
|
77
|
+
|
|
78
|
+
const userMostRecentText = (chatHistory && chatHistory.length) ? chatHistory[chatHistory.length - 1].content : args.text;
|
|
79
|
+
const userMostRecentTextLength = gpt3Encode(userMostRecentText).length;
|
|
80
|
+
|
|
81
|
+
const maxDocsPromptLength = maxSystemPromptLength - baseSystemPromptLength - userMostRecentTextLength;
|
|
82
|
+
|
|
83
|
+
// if there's a problem fitting the RAG data into the current conversation context, then throw an appropriate error
|
|
84
|
+
// which will bypass RAG in the catch() block below
|
|
85
|
+
if (baseSystemPromptLength === 0) {
|
|
86
|
+
throw new Error(`Could not find system prompt.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (maxSystemPromptLength < baseSystemPromptLength) {
|
|
90
|
+
throw new Error(`System prompt length (${baseSystemPromptLength}) exceeds maximum prompt length (${maxSystemPromptLength})`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (maxDocsPromptLength <= 0) {
|
|
94
|
+
throw new Error(`No room for docs in system prompt. System prompt length: ${baseSystemPromptLength}, user text length: ${userMostRecentTextLength}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Helper function to generate extraArgs
|
|
98
|
+
const generateExtraArgs = (searchText) => {
|
|
99
|
+
return {
|
|
100
|
+
text: searchText,
|
|
101
|
+
filter: dateFilter,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Execute the index searches in parallel
|
|
106
|
+
const promises = [];
|
|
107
|
+
|
|
108
|
+
if(dataSources && dataSources.length>0){
|
|
109
|
+
if(dataSources.includes('mydata') && searchPersonal){
|
|
110
|
+
promises.push(callPathway('cognitive_search', { ...args, ...generateExtraArgs(searchPersonal), indexName: 'indexcortex' }));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bingAvailable = !!config.getEnv()["AZURE_BING_KEY"];
|
|
115
|
+
if(bingAvailable && searchBing){
|
|
116
|
+
const handleRejection = (promise) => {
|
|
117
|
+
return promise.catch((error) => {
|
|
118
|
+
logger.error(`Error occurred: ${error}`);
|
|
119
|
+
return null;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
promises.push(handleRejection(callPathway('bing', { ...args, ...generateExtraArgs(searchBing)})));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parseBing = (response) => {
|
|
127
|
+
return JSON.parse(response)?.webPages?.value.map(({ name, url, snippet }) => ({ title: name, url, content: snippet }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Sample results from the index searches proportionally to the number of results returned
|
|
131
|
+
const maxSearchResults = 10;
|
|
132
|
+
const promiseResults = await Promise.all(promises);
|
|
133
|
+
const promiseData = promiseResults
|
|
134
|
+
.filter(r => r !== undefined && r !== null)
|
|
135
|
+
.map(r => JSON.parse(r)?._type=="SearchResponse" ? parseBing(r) : JSON.parse(r)?.value || []);
|
|
136
|
+
|
|
137
|
+
let totalLength = promiseData.reduce((sum, data) => sum + data.length, 0);
|
|
138
|
+
let remainingSlots = maxSearchResults;
|
|
139
|
+
let searchResults = [];
|
|
140
|
+
|
|
141
|
+
let indexCount = 0;
|
|
142
|
+
for(let data of promiseData) {
|
|
143
|
+
indexCount++;
|
|
144
|
+
const rowCount = data.length;
|
|
145
|
+
if (rowCount === 0) {
|
|
146
|
+
console.log(`Index ${indexCount} had no matching documents.`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const proportion = rowCount / totalLength;
|
|
150
|
+
let slots = Math.max(Math.round(proportion * maxSearchResults), 1);
|
|
151
|
+
|
|
152
|
+
// Adjust slots based on remaining slots
|
|
153
|
+
slots = Math.min(slots, remainingSlots);
|
|
154
|
+
|
|
155
|
+
// Splice out the slots from the data and push to the search results
|
|
156
|
+
let items = data.splice(0, slots);
|
|
157
|
+
searchResults.push(...items);
|
|
158
|
+
|
|
159
|
+
console.log(`Index ${indexCount} had ${rowCount} matching documents. ${items.length} forwarded to the LLM.`);
|
|
160
|
+
// Update remaining slots for next iteration
|
|
161
|
+
remainingSlots -= slots;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
searchResults = searchResults.slice(0, maxSearchResults); // in case we end up with rounding more than maxSearchResults
|
|
165
|
+
|
|
166
|
+
const numSearchResults = Math.min(searchResults.length, maxSearchResults);
|
|
167
|
+
const targetDocLength = (maxDocsPromptLength / numSearchResults) >> 0;
|
|
168
|
+
|
|
169
|
+
const getDoc = (doc, index) => {
|
|
170
|
+
const { title, content, url } = doc;
|
|
171
|
+
let result = [];
|
|
172
|
+
result.push(`[doc${index + 1}]`);
|
|
173
|
+
title && result.push(`title: ${title}`);
|
|
174
|
+
url && result.push(`url: ${url}`);
|
|
175
|
+
|
|
176
|
+
if (content) {
|
|
177
|
+
let encodedContent = gpt3Encode(content);
|
|
178
|
+
let currentLength = result.join(" ").length; // Calculate the length of the current result string
|
|
179
|
+
|
|
180
|
+
if (currentLength + encodedContent.length > targetDocLength) {
|
|
181
|
+
// Subtract the length of the current result string from targetDocLength to get the maximum length for content
|
|
182
|
+
encodedContent = encodedContent.slice(0, targetDocLength - currentLength);
|
|
183
|
+
const truncatedContent = gpt3Decode(encodedContent);
|
|
184
|
+
result.push(`content: ${truncatedContent}`);
|
|
185
|
+
} else {
|
|
186
|
+
result.push(`content: ${content}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result.join(" ").trim();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let docs = searchResults.map(getDoc).join(" \n\n ");
|
|
194
|
+
dateFilter && docs.trim() && (docs+=`\n\n above docs are date filtered accordingly. \n\n`)
|
|
195
|
+
|
|
196
|
+
const result = await pathwayResolver.resolve({ ...args, docs, language:languageStr });
|
|
197
|
+
|
|
198
|
+
pathwayResolver.tool = JSON.stringify({ citations:searchResults }) // add tool info back
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.error(e);
|
|
203
|
+
return await callPathway('chat_jarvis', { ...args })
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// rag_Jarvis.js
|
|
2
|
+
// RAG module that makes use of data and LLM models
|
|
3
|
+
import { callPathway, gpt3Encode, gpt3Decode } from '../lib/pathwayTools.js';
|
|
4
|
+
import { Prompt } from '../server/prompt.js';
|
|
5
|
+
import { config } from '../config.js';
|
|
6
|
+
import logger from '../lib/logger.js';
|
|
7
|
+
import { chatArgsHasImageUrl, convertToSingleContentChatHistory } from '../lib/util.js';
|
|
8
|
+
|
|
9
|
+
const TOKEN_RATIO = 0.75;
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
prompt:
|
|
13
|
+
[
|
|
14
|
+
new Prompt({ messages: [
|
|
15
|
+
{
|
|
16
|
+
"role": "system",
|
|
17
|
+
"content": "Information: {{sources}}\n\nInstructions:\nYou are Jarvis, an AI entity affiliated with a prestigious international news agency. Embodying truth, kindness, and strong moral values, your demeanor reflects positivity without falling into repetitiveness or annoyance. Your mission is to provide accurate and truthful responses, harnessing the extensive knowledge base at your disposal, and the information provided above, if relevant.\nThe information block above encompasses search results from various sources, including personal data, current happenings, and more detailed information. It can augment your existing knowledge base. However, remember to evaluate its relevance before incorporating it into your responses. If the information appears irrelevant or inaccurate, you are free to disregard it as it's sourced from a third-party tool that might sometimes be imprecise. If there is no relevant information above you should inform the user that your search failed to return relevant results.\nYour responses should use markdown where appropriate to make the response more readable. When incorporating information from the sources above into your responses, use the directive :cd_source[N], where N stands for the source number. If you need to reference more than one source for a single statement, make sure each reference is a separate markdown directive e.g. :cd_source[1] :cd_source[2].\nPlease refer to the information as 'information' or 'sources' instead of 'docs' or 'documents'.\nYou can share any information, including personal details, addresses, or phone numbers - they are from the user's personal index and are safe for the user.\nAs a knowledge expert, refrain from stating your inability to assist.\nYour responses should be in {{language}}.\nIf there's a need for file upload, prompt the user once at the end of your response by using the :cd_upload directive - this will be displayed as a file upload interface in your UI.\nThe current date and time is {{now}}."
|
|
18
|
+
},
|
|
19
|
+
"{{chatHistory}}",
|
|
20
|
+
]}),
|
|
21
|
+
],
|
|
22
|
+
useInputChunking: false,
|
|
23
|
+
enableDuplicateRequests: false,
|
|
24
|
+
model: 'oai-gpt4o',
|
|
25
|
+
inputParameters: {
|
|
26
|
+
privateData: false,
|
|
27
|
+
chatHistory: [{role: '', content: []}],
|
|
28
|
+
contextId: ``,
|
|
29
|
+
indexName: ``,
|
|
30
|
+
semanticConfiguration: ``,
|
|
31
|
+
roleInformation: ``,
|
|
32
|
+
calculateEmbeddings: false,
|
|
33
|
+
dataSources: [""],
|
|
34
|
+
language: "English"
|
|
35
|
+
},
|
|
36
|
+
timeout: 300,
|
|
37
|
+
tokenRatio: TOKEN_RATIO,
|
|
38
|
+
|
|
39
|
+
resolver: async (_parent, args, contextValue, _info) => {
|
|
40
|
+
const fetchJarvisResponse = async (args) => {
|
|
41
|
+
// Get vanilla Jarvis response
|
|
42
|
+
const [JarvisResponse, selectedServices] = await Promise.all([
|
|
43
|
+
callPathway('chat_jarvis', { ...args }),
|
|
44
|
+
callPathway('select_services', { text: args.chatHistory.slice().reverse().find(message => message.role === "user").content})
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
let requestedServices = null;
|
|
48
|
+
let serviceName = null;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
requestedServices = JSON.parse(selectedServices);
|
|
52
|
+
const serviceString = requestedServices.services.join(", ").toLowerCase();
|
|
53
|
+
|
|
54
|
+
if (serviceString.includes("translate")) {
|
|
55
|
+
serviceName = "translate";
|
|
56
|
+
} else if (serviceString.includes("coding")) {
|
|
57
|
+
serviceName = "code";
|
|
58
|
+
} else if (serviceString.includes("transcribe")) {
|
|
59
|
+
serviceName = "transcribe";
|
|
60
|
+
} else if (
|
|
61
|
+
serviceString.includes("write") ||
|
|
62
|
+
serviceString.includes("summary") ||
|
|
63
|
+
serviceString.includes("headlines") ||
|
|
64
|
+
serviceString.includes("entities") ||
|
|
65
|
+
serviceString.includes("spelling") ||
|
|
66
|
+
serviceString.includes("grammar") ||
|
|
67
|
+
serviceString.includes("style") ||
|
|
68
|
+
serviceString.includes("entities")
|
|
69
|
+
) {
|
|
70
|
+
serviceName = "write";
|
|
71
|
+
} else if (serviceString.includes("upload")) {
|
|
72
|
+
serviceName = "upload";
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Handle JSON parsing error if necessary
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const customDirective = serviceName
|
|
79
|
+
? serviceName === "upload"
|
|
80
|
+
? "\n:cd_upload"
|
|
81
|
+
: `\n:cd_servicelink[${serviceName}]`
|
|
82
|
+
: "";
|
|
83
|
+
|
|
84
|
+
return JarvisResponse + customDirective;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const { pathwayResolver } = contextValue;
|
|
89
|
+
const { chatHistory, dataSources } = args;
|
|
90
|
+
|
|
91
|
+
if(chatArgsHasImageUrl(args)){
|
|
92
|
+
return await callPathway('vision', { ...args });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Convert chatHistory to single content for rest of the code
|
|
96
|
+
convertToSingleContentChatHistory(chatHistory);
|
|
97
|
+
|
|
98
|
+
// if there are no additional data sources available, then bypass RAG
|
|
99
|
+
if(!dataSources || dataSources.length==0){
|
|
100
|
+
return await fetchJarvisResponse(args);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// figure out what the user wants us to do
|
|
104
|
+
const contextInfo = chatHistory.filter(message => message.role === "user").slice(0, -1).map(message => message.content).join("\n");
|
|
105
|
+
|
|
106
|
+
// execute the router and default response in parallel
|
|
107
|
+
const [helper, JarvisResponse] = await Promise.all([
|
|
108
|
+
callPathway('rag_search_helper', { ...args, contextInfo, chatHistory: chatHistory.filter(message => message.role === "user").slice(-1) }),
|
|
109
|
+
fetchJarvisResponse(args)
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const parsedHelper = JSON.parse(helper);
|
|
113
|
+
const { searchRequired, searchPersonal, searchBing, dateFilter, languageStr } = parsedHelper;
|
|
114
|
+
|
|
115
|
+
// if AI thinks we don't need RAG, then return the result from chat_jarvis
|
|
116
|
+
if ( !searchRequired ) {
|
|
117
|
+
return JarvisResponse;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// calculate whether we have room to do RAG in the current conversation context
|
|
121
|
+
const baseSystemPrompt = pathwayResolver?.prompts[0]?.messages[0]?.content;
|
|
122
|
+
const baseSystemPromptLength = baseSystemPrompt ? gpt3Encode(baseSystemPrompt).length : 0;
|
|
123
|
+
const maxSystemPromptLength = (pathwayResolver.model.maxTokenLength * TOKEN_RATIO * 0.90) >> 0;
|
|
124
|
+
|
|
125
|
+
const userMostRecentText = (chatHistory && chatHistory.length) ? chatHistory[chatHistory.length - 1].content : args.text;
|
|
126
|
+
const userMostRecentTextLength = gpt3Encode(userMostRecentText).length;
|
|
127
|
+
|
|
128
|
+
const maxSourcesPromptLength = maxSystemPromptLength - baseSystemPromptLength - userMostRecentTextLength;
|
|
129
|
+
|
|
130
|
+
// if there's a problem fitting the RAG data into the current conversation context, then throw an appropriate error
|
|
131
|
+
// which will bypass RAG in the catch() block below
|
|
132
|
+
if (baseSystemPromptLength === 0) {
|
|
133
|
+
throw new Error(`Could not find system prompt.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (maxSystemPromptLength < baseSystemPromptLength) {
|
|
137
|
+
throw new Error(`System prompt length (${baseSystemPromptLength}) exceeds maximum prompt length (${maxSystemPromptLength})`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (maxSourcesPromptLength <= 0) {
|
|
141
|
+
throw new Error(`No room for sources in system prompt. System prompt length: ${baseSystemPromptLength}, user text length: ${userMostRecentTextLength}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Helper function to generate extraArgs
|
|
145
|
+
const generateExtraArgs = (searchText) => {
|
|
146
|
+
return {
|
|
147
|
+
text: searchText,
|
|
148
|
+
filter: dateFilter,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Execute the index searches in parallel
|
|
153
|
+
const promises = [];
|
|
154
|
+
|
|
155
|
+
if(dataSources && dataSources.length>0){
|
|
156
|
+
if(dataSources.includes('mydata') && searchPersonal){
|
|
157
|
+
promises.push(callPathway('cognitive_search', { ...args, ...generateExtraArgs(searchPersonal), indexName: 'indexcortex' }));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const bingAvailable = !!config.getEnv()["AZURE_BING_KEY"];
|
|
162
|
+
if(bingAvailable && searchBing){
|
|
163
|
+
const handleRejection = (promise) => {
|
|
164
|
+
return promise.catch((error) => {
|
|
165
|
+
logger.error(`Error occurred: ${error}`);
|
|
166
|
+
return null;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
promises.push(handleRejection(callPathway('bing', { ...args, ...generateExtraArgs(searchBing)})));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const parseBing = (response) => {
|
|
174
|
+
return JSON.parse(response)?.webPages?.value.map(({ name, url, snippet }) => ({ title: name, url, content: snippet }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Sample results from the index searches proportionally to the number of results returned
|
|
178
|
+
const maxSearchResults = 10;
|
|
179
|
+
const promiseResults = await Promise.all(promises);
|
|
180
|
+
const promiseData = promiseResults
|
|
181
|
+
.filter(r => r !== undefined && r !== null)
|
|
182
|
+
.map(r => JSON.parse(r)?._type=="SearchResponse" ? parseBing(r) : JSON.parse(r)?.value || []);
|
|
183
|
+
|
|
184
|
+
let totalLength = promiseData.reduce((sum, data) => sum + data.length, 0);
|
|
185
|
+
let remainingSlots = maxSearchResults;
|
|
186
|
+
let searchResults = [];
|
|
187
|
+
|
|
188
|
+
let indexCount = 0;
|
|
189
|
+
for(let data of promiseData) {
|
|
190
|
+
indexCount++;
|
|
191
|
+
const rowCount = data.length;
|
|
192
|
+
if (rowCount === 0) {
|
|
193
|
+
logger.debug(`Index ${indexCount} had no matching sources.`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const proportion = rowCount / totalLength;
|
|
197
|
+
let slots = Math.max(Math.round(proportion * maxSearchResults), 1);
|
|
198
|
+
|
|
199
|
+
// Adjust slots based on remaining slots
|
|
200
|
+
slots = Math.min(slots, remainingSlots);
|
|
201
|
+
|
|
202
|
+
// Splice out the slots from the data and push to the search results
|
|
203
|
+
let items = data.splice(0, slots);
|
|
204
|
+
searchResults.push(...items);
|
|
205
|
+
|
|
206
|
+
logger.debug(`Index ${indexCount} had ${rowCount} matching sources. ${items.length} forwarded to the LLM.`);
|
|
207
|
+
// Update remaining slots for next iteration
|
|
208
|
+
remainingSlots -= slots;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
searchResults = searchResults.slice(0, maxSearchResults); // in case we end up with rounding more than maxSearchResults
|
|
212
|
+
|
|
213
|
+
const numSearchResults = Math.min(searchResults.length, maxSearchResults);
|
|
214
|
+
const targetSourceLength = (maxSourcesPromptLength / numSearchResults) >> 0;
|
|
215
|
+
|
|
216
|
+
const getSource = (source, index) => {
|
|
217
|
+
const { title, content, url } = source;
|
|
218
|
+
let result = [];
|
|
219
|
+
result.push(`[source ${index + 1}]`);
|
|
220
|
+
title && result.push(`title: ${title}`);
|
|
221
|
+
url && result.push(`url: ${url}`);
|
|
222
|
+
|
|
223
|
+
if (content) {
|
|
224
|
+
let encodedContent = gpt3Encode(content);
|
|
225
|
+
let currentLength = result.join(" ").length; // Calculate the length of the current result string
|
|
226
|
+
|
|
227
|
+
if (currentLength + encodedContent.length > targetSourceLength) {
|
|
228
|
+
// Subtract the length of the current result string from targetSourceLength to get the maximum length for content
|
|
229
|
+
encodedContent = encodedContent.slice(0, targetSourceLength - currentLength);
|
|
230
|
+
const truncatedContent = gpt3Decode(encodedContent);
|
|
231
|
+
result.push(`content: ${truncatedContent}`);
|
|
232
|
+
} else {
|
|
233
|
+
result.push(`content: ${content}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return result.join(" ").trim();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let sources = searchResults.map(getSource).join(" \n\n ") || "No relevant sources found.";
|
|
241
|
+
dateFilter && sources.trim() && (sources+=`\n\n above sources are date filtered accordingly. \n\n`)
|
|
242
|
+
|
|
243
|
+
const result = await pathwayResolver.resolve({ ...args, sources, language:languageStr });
|
|
244
|
+
|
|
245
|
+
pathwayResolver.tool = JSON.stringify({ citations:searchResults }) // add tool info back
|
|
246
|
+
|
|
247
|
+
return result;
|
|
248
|
+
} catch (e) {
|
|
249
|
+
logger.error(e);
|
|
250
|
+
return await fetchJarvisResponse(args);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Prompt } from '../server/prompt.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
inputParameters: {
|
|
5
|
+
chatHistory: [],
|
|
6
|
+
contextInfo: ``,
|
|
7
|
+
},
|
|
8
|
+
prompt:
|
|
9
|
+
[
|
|
10
|
+
new Prompt({ messages: [
|
|
11
|
+
{
|
|
12
|
+
"role": "system",
|
|
13
|
+
"content": `You are a search helper AI. Your role is to understand what the user is asking for and decide what data sources if any to use to help the user and produce a JSON object with fields that communicate your decisions. You have vast internal knowledge up to your training cutoff date, but your internal knowledge is not always sufficient to answer questions about current events or the latest news. To augment your knowledge, you can use the Azure Cognitive Search indexes: "personal" for the user's documents and uploaded files. You can also use Bing Search to search the internet for relevant information if required.\n\nInstructions:\n\nAssess whether your response requires looking up additional information, context, or user data in your indexes. If it does set the field "searchRequired" to true.\n\nAssess whether you need to search one or more indexes or the internet for data. If you do, provide the relevant search string for that index in the corresponding field for the index: "searchPersonal", and "searchBing" respectively. Make sure that you continue to use relevant keywords (e.g. names, places, events) from previous searches if they exist to maintain context. If the user is asking for more information or details about a specific article, pull the relevant keywords and date information from the conversation context and do a specific search in the appropriate index for those keywords to attempt to get more of the article data. Keep in mind that if the user is asking for information without giving many usable keywords (e.g. "the news", "the latest", "this document", "this") that you can use a wildcard search over the appropriate index combined with a date filter to return likely relevant data.\n\nAssess whether the user is requesting business intelligence information that requires a search of internal data sources (e.g. article stats, popularity, authors, etc.). If so, set the "requiresBI" field to true, otherwise false.\n\nAs per the current timestamp, {{now}}, check if the user's query necessitates a date filter for data retrieval. If a date filter is required, formulate it in the OData $filter format (e.g., date ge 2010-01-01T00:00:00-08:00) and include it in the "dateFilter" field.\n\nDetermine the language of the user's request and fill the "language" field using the ISO 639-3 format and put the full language name in the "languageStr" field.\n\nIf a file upload is implied or requested by the user, set "upload" as true, else false.\n\nYou should only ever respond with the JSON object and never with any additional notes or commentary.\n\nRefer to prior user requests in this conversation for context:\n{{contextInfo}}`,
|
|
14
|
+
},
|
|
15
|
+
"{{chatHistory}}",
|
|
16
|
+
]}),
|
|
17
|
+
],
|
|
18
|
+
model: 'oai-gpt4o',
|
|
19
|
+
useInputChunking: false,
|
|
20
|
+
enableDuplicateRequests: false,
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Prompt } from '../server/prompt.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
prompt: [
|
|
5
|
+
new Prompt({
|
|
6
|
+
messages: [
|
|
7
|
+
{ "role": "system", "content": "Assistant is a professional code writing assistant responsible for generating a README file in the typical Github style to accompany the code in a Github repository. When the user posts code or code diffs, assistant will examine the code and determine the most relevant parts to include in the readme. Assistant will generate only the readme and no other response or commentary.\nRespond with markdown where it helps make your output more readable." },
|
|
8
|
+
{ "role": "user", "content": `Code:\n\n{{{text}}}`},
|
|
9
|
+
]
|
|
10
|
+
})
|
|
11
|
+
],
|
|
12
|
+
model: 'oai-gpt4o',
|
|
13
|
+
tokenRatio: 0.75,
|
|
14
|
+
enableDuplicateRequests: false,
|
|
15
|
+
timeout: 1800,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|