Compare commits

...

13 Commits

Author SHA1 Message Date
Emanuel Ferreira a517415034 test 2024-03-26 20:01:47 -03:00
Emanuel Ferreira 865af67610 chore: temp settings 2024-03-26 19:57:35 -03:00
Emanuel Ferreira 5869b75959 chore: temp settings 2024-03-26 19:56:28 -03:00
Emanuel Ferreira 2bbd641c0a chore: update examples 2024-03-26 18:37:57 -03:00
Emanuel Ferreira 01482a2038 fix: circular dependency 2024-03-26 18:16:08 -03:00
Emanuel Ferreira 6eea759c05 Merge branch 'main' of github.com:run-llama/LlamaIndexTS into feat/prompt-mustache 2024-03-26 18:13:17 -03:00
Emanuel Ferreira efa5b01a63 wip 2024-03-25 15:11:43 -03:00
Emanuel Ferreira bc48f8f30b refactor: evaluators 2024-03-24 18:08:01 -03:00
Emanuel Ferreira 6a4fe6ee3d feat: initial global settings 2024-03-22 08:03:45 -03:00
Emanuel Ferreira 52f5c9c58f wip 2024-03-21 19:05:36 -03:00
Emanuel Ferreira 5cbe6ef871 mustach 2024-03-21 09:56:46 -03:00
Emanuel Ferreira 9b740c5144 builders 2024-03-20 22:17:08 -03:00
Emanuel Ferreira ad9707e479 wip 2024-03-20 22:01:46 -03:00
25 changed files with 683 additions and 348 deletions
+6 -6
View File
@@ -1,5 +1,6 @@
import {
Document,
Prompt,
ResponseSynthesizer,
TreeSummarize,
TreeSummarizePrompt,
@@ -7,16 +8,15 @@ import {
serviceContextFromDefaults,
} from "llamaindex";
const treeSummarizePrompt: TreeSummarizePrompt = ({ context, query }) => {
return `Context information from multiple sources is below.
const treeSummarizePrompt: TreeSummarizePrompt =
new Prompt(`Context information from multiple sources is below.
---------------------
${context}
{{context}}
---------------------
Given the information from multiple sources and not prior knowledge.
Answer the query in the style of a Shakespeare play"
Query: ${query}
Answer:`;
};
Query: {{query}}
Answer:`);
async function main() {
const documents = new Document({
+6 -7
View File
@@ -1,9 +1,10 @@
import {
CompactAndRefine,
OpenAI,
Prompt,
ResponseSynthesizer,
serviceContextFromDefaults,
VectorStoreIndex,
serviceContextFromDefaults,
} from "llamaindex";
import { PapaCSVReader } from "llamaindex/readers";
@@ -22,14 +23,12 @@ async function main() {
serviceContext,
});
const csvPrompt = ({ context = "", query = "" }) => {
return `The following CSV file is loaded from ${path}
const csvPrompt = new Prompt(`The following CSV file is loaded from {{path}}
\`\`\`csv
${context}
{{context}}
\`\`\`
Given the CSV file, generate me Typescript code to answer the question: ${query}. You can use built in NodeJS functions but avoid using third party libraries.
`;
};
Given the CSV file, generate me Typescript code to answer the question: {{query}. You can use built in NodeJS functions but avoid using third party libraries.
`);
const responseSynthesizer = new ResponseSynthesizer({
responseBuilder: new CompactAndRefine(serviceContext, csvPrompt),
+5 -6
View File
@@ -2,14 +2,16 @@ import fs from "node:fs/promises";
import {
Anthropic,
anthropicTextQaPrompt,
CompactAndRefine,
Document,
ResponseSynthesizer,
serviceContextFromDefaults,
Settings,
VectorStoreIndex,
serviceContextFromDefaults,
} from "llamaindex";
Settings.prompt.llm = "claude";
async function main() {
// Load essay from abramov.txt in Node
const path = "node_modules/llamaindex/examples/abramov.txt";
@@ -23,10 +25,7 @@ async function main() {
const serviceContext = serviceContextFromDefaults({ llm: new Anthropic() });
const responseSynthesizer = new ResponseSynthesizer({
responseBuilder: new CompactAndRefine(
serviceContext,
anthropicTextQaPrompt,
),
responseBuilder: new CompactAndRefine(serviceContext),
});
const index = await VectorStoreIndex.fromDocuments([document], {
+4 -1
View File
@@ -30,6 +30,7 @@
"mammoth": "^1.6.0",
"md-utils-ts": "^2.0.0",
"mongodb": "^6.3.0",
"mustache": "^4.2.0",
"notion-md-crawler": "^0.0.2",
"openai": "^4.26.1",
"papaparse": "^5.4.1",
@@ -41,12 +42,14 @@
"rake-modified": "^1.0.8",
"replicate": "^0.25.2",
"string-strip-html": "^13.4.6",
"wikipedia": "^2.1.2",
"wink-nlp": "^1.14.3",
"wikipedia": "^2.1.2"
"yaml": "^2.4.1"
},
"devDependencies": {
"@swc/cli": "^0.3.9",
"@swc/core": "^1.4.2",
"@types/mustache": "^4.2.5",
"concurrently": "^8.2.2",
"glob": "^10.3.10",
"madge": "^6.1.0",
+1 -1
View File
@@ -96,7 +96,7 @@ export class SummaryChatHistory extends ChatHistory {
do {
promptMessages = [
{
content: this.summaryPrompt({
content: this.summaryPrompt.format({
context: messagesToHistoryStr(messagesToSummarize),
}),
role: "user" as MessageType,
+79 -102
View File
@@ -1,5 +1,8 @@
import type { SubQuestion } from "./engines/query/types.js";
import type { ChatMessage } from "./llm/types.js";
import { Prompt } from "./prompts/types.js";
import type { ToolMetadata } from "./types.js";
/**
@@ -24,67 +27,42 @@ DEFAULT_TEXT_QA_PROMPT_TMPL = (
)
*/
export const defaultTextQaPrompt = ({ context = "", query = "" }) => {
return `Context information is below.
---------------------
${context}
---------------------
Given the context information and not prior knowledge, answer the query.
Query: ${query}
Answer:`;
};
export const defaultTextQaPrompt = new Prompt(`
default: >
Context information is below.
---------------------
{{context}}
---------------------
Given the context information and not prior knowledge, answer the query.
Query: {{query}}
Answer:
llm-claude: >
Context information:
<context>
{{context}}
</context>
Given the context information and not prior knowledge, answer the query.
Query: {{query}};
`);
export type TextQaPrompt = typeof defaultTextQaPrompt;
export const defaultSummaryPrompt = new Prompt(`
default: >
Write a summary of the following. Try to use only the information provided. Try to include as many key details as possible.
export const anthropicTextQaPrompt: TextQaPrompt = ({
context = "",
query = "",
}) => {
return `Context information:
<context>
${context}
</context>
Given the context information and not prior knowledge, answer the query.
Query: ${query}`;
};
{{context}}
/*
DEFAULT_SUMMARY_PROMPT_TMPL = (
"Write a summary of the following. Try to use only the "
"information provided. "
"Try to include as many key details as possible.\n"
"\n"
"\n"
"{context_str}\n"
"\n"
"\n"
'SUMMARY:"""\n'
)
*/
SUMMARY:"""
llm-claude: >
Summarize the following text. Try to use only the information provided. Try to include as many key details as possible.
<original-text>
{{context}}
</original-text>
export const defaultSummaryPrompt = ({ context = "" }) => {
return `Write a summary of the following. Try to use only the information provided. Try to include as many key details as possible.
${context}
SUMMARY:"""
`;
};
SUMMARY:
`);
export type SummaryPrompt = typeof defaultSummaryPrompt;
export const anthropicSummaryPrompt: SummaryPrompt = ({ context = "" }) => {
return `Summarize the following text. Try to use only the information provided. Try to include as many key details as possible.
<original-text>
${context}
</original-text>
SUMMARY:
`;
};
/*
DEFAULT_REFINE_PROMPT_TMPL = (
"The original query is as follows: {query_str}\n"
@@ -101,20 +79,17 @@ DEFAULT_REFINE_PROMPT_TMPL = (
)
*/
export const defaultRefinePrompt = ({
query = "",
existingAnswer = "",
context = "",
}) => {
return `The original query is as follows: ${query}
We have provided an existing answer: ${existingAnswer}
We have the opportunity to refine the existing answer (only if needed) with some more context below.
------------
${context}
------------
Given the new context, refine the original answer to better answer the query. If the context isn't useful, return the original answer.
Refined Answer:`;
};
export const defaultRefinePrompt = new Prompt(`
default: >
The original query is as follows: {{query}}
We have provided an existing answer: {{existingAnswer}}
We have the opportunity to refine the existing answer (only if needed) with some more context below.
------------
{{context}}
------------
Given the new context, refine the original answer to better answer the query. If the context isn't useful, return the original answer.
Refined Answer:
`);
export type RefinePrompt = typeof defaultRefinePrompt;
@@ -131,49 +106,51 @@ DEFAULT_TREE_SUMMARIZE_TMPL = (
)
*/
export const defaultTreeSummarizePrompt = ({ context = "", query = "" }) => {
return `Context information from multiple sources is below.
---------------------
${context}
---------------------
Given the information from multiple sources and not prior knowledge, answer the query.
Query: ${query}
Answer:`;
};
export const defaultTreeSummarizePrompt = new Prompt(`
default: >
Context information from multiple sources is below.
---------------------
{{context}}
---------------------
Given the information from multiple sources and not prior knowledge, answer the query.
Query: {{query}}
Answer:
`);
export type TreeSummarizePrompt = typeof defaultTreeSummarizePrompt;
export const defaultChoiceSelectPrompt = ({ context = "", query = "" }) => {
return `A list of documents is shown below. Each document has a number next to it along
with a summary of the document. A question is also provided.
Respond with the numbers of the documents
you should consult to answer the question, in order of relevance, as well
as the relevance score. The relevance score is a number from 1-10 based on
how relevant you think the document is to the question.
Do not include any documents that are not relevant to the question.
Example format:
Document 1:
<summary of document 1>
export const defaultChoiceSelectPrompt = new Prompt(`
default: >
A list of documents is shown below. Each document has a number next to it along
with a summary of the document. A question is also provided.
Respond with the numbers of the documents
you should consult to answer the question, in order of relevance, as well
as the relevance score. The relevance score is a number from 1-10 based on
how relevant you think the document is to the question.
Do not include any documents that are not relevant to the question.
Example format:
Document 1:
<summary of document 1>
Document 2:
<summary of document 2>
Document 2:
<summary of document 2>
...
...
Document 10:\n<summary of document 10>
Document 10:\n<summary of document 10>
Question: <question>
Answer:
Doc: 9, Relevance: 7
Doc: 3, Relevance: 4
Doc: 7, Relevance: 3
Question: <question>
Answer:
Doc: 9, Relevance: 7
Doc: 3, Relevance: 4
Doc: 7, Relevance: 3
Let's try this now:
Let's try this now:
${context}
Question: ${query}
Answer:`;
};
{{context}}
Question: {{query}}
Answer:
`);
export type ChoiceSelectPrompt = typeof defaultChoiceSelectPrompt;
+17 -16
View File
@@ -1,5 +1,4 @@
import { globalsHelper } from "./GlobalsHelper.js";
import type { SimplePrompt } from "./Prompt.js";
import { SentenceSplitter } from "./TextSplitter.js";
import {
DEFAULT_CHUNK_OVERLAP_RATIO,
@@ -8,8 +7,18 @@ import {
DEFAULT_PADDING,
} from "./constants.js";
export function getEmptyPromptTxt(prompt: SimplePrompt) {
return prompt({});
import { Prompt } from "./prompts/types.js";
import { messagesToPrompt } from "./prompts/utils.js";
export function getEmptyPromptTxt(prompt: Prompt) {
const emptyPrompt = prompt.format({});
if (Array.isArray(emptyPrompt)) {
return messagesToPrompt(emptyPrompt);
}
return emptyPrompt;
}
/**
@@ -18,7 +27,7 @@ export function getEmptyPromptTxt(prompt: SimplePrompt) {
* @param prompts
* @returns
*/
export function getBiggestPrompt(prompts: SimplePrompt[]) {
export function getBiggestPrompt(prompts: Prompt[]) {
const emptyPromptTexts = prompts.map(getEmptyPromptTxt);
const emptyPromptLengths = emptyPromptTexts.map((text) => text.length);
const maxEmptyPromptLength = Math.max(...emptyPromptLengths);
@@ -59,7 +68,7 @@ export class PromptHelper {
* @param prompt
* @returns
*/
private getAvailableContextSize(prompt: SimplePrompt) {
private getAvailableContextSize(prompt: Prompt) {
const emptyPromptText = getEmptyPromptTxt(prompt);
const promptTokens = this.tokenizer(emptyPromptText);
const numPromptTokens = promptTokens.length;
@@ -74,11 +83,7 @@ export class PromptHelper {
* @param padding
* @returns
*/
private getAvailableChunkSize(
prompt: SimplePrompt,
numChunks = 1,
padding = 5,
) {
private getAvailableChunkSize(prompt: Prompt, numChunks = 1, padding = 5) {
const availableContextSize = this.getAvailableContextSize(prompt);
const result = Math.floor(availableContextSize / numChunks) - padding;
@@ -98,7 +103,7 @@ export class PromptHelper {
* @returns
*/
getTextSplitterGivenPrompt(
prompt: SimplePrompt,
prompt: Prompt,
numChunks = 1,
padding = DEFAULT_PADDING,
) {
@@ -118,11 +123,7 @@ export class PromptHelper {
* @param padding
* @returns
*/
repack(
prompt: SimplePrompt,
textChunks: string[],
padding = DEFAULT_PADDING,
) {
repack(prompt: Prompt, textChunks: string[], padding = DEFAULT_PADDING) {
const textSplitter = this.getTextSplitterGivenPrompt(prompt, 1, padding);
const combinedStr = textChunks.join("\n\n");
return textSplitter.splitText(combinedStr);
+27
View File
@@ -0,0 +1,27 @@
type PromptConfig = {
llm?: string;
lang?: string;
};
interface Config {
prompt: PromptConfig;
}
// Determine the global object based on the environment
const globalObject: any =
typeof window !== "undefined"
? window
: typeof global !== "undefined"
? global
: {};
// Initialize or access a global config object
const globalConfigKey = "__GLOBAL_LITS__";
if (!globalObject[globalConfigKey]) {
globalObject[globalConfigKey] = {
prompt: {},
} satisfies Config;
}
export const Settings: Config = globalObject[globalConfigKey];
+2 -2
View File
@@ -68,11 +68,11 @@ export class CorrectnessEvaluator extends PromptMixin implements BaseEvaluator {
const messages: ChatMessage[] = [
{
role: "system",
content: this.correctnessPrompt(),
content: this.correctnessPrompt.format({}),
},
{
role: "user",
content: defaultUserPrompt({
content: defaultUserPrompt.format({
query,
generatedAnswer: response,
referenceAnswer: reference || "(NO REFERENCE ANSWER SUPPLIED)",
+105 -130
View File
@@ -1,155 +1,130 @@
export const defaultUserPrompt = ({
query,
referenceAnswer,
generatedAnswer,
}: {
query: string;
referenceAnswer: string;
generatedAnswer: string;
}) => `
## User Query
${query}
import { Prompt } from "./../prompts/types.js";
## Reference Answer
${referenceAnswer}
export const defaultUserPrompt = new Prompt(`
default: >
## User Query
{{query}}
## Generated Answer
${generatedAnswer}
`;
## Reference Answer
{{referenceAnswer}}
## Generated Answer
{{generatedAnswer}}
`);
export type UserPrompt = typeof defaultUserPrompt;
export const defaultCorrectnessSystemPrompt =
() => `You are an expert evaluation system for a question answering chatbot.
export const defaultCorrectnessSystemPrompt = new Prompt(`
default: >
You are an expert evaluation system for a question answering chatbot.
You are given the following information:
- a user query, and
- a generated answer
You are given the following information:
- a user query, and
- a generated answer
You may also be given a reference answer to use for reference in your evaluation.
You may also be given a reference answer to use for reference in your evaluation.
Your job is to judge the relevance and correctness of the generated answer.
Output a single score that represents a holistic evaluation.
You must return your response in a line with only the score.
Do not return answers in any other format.
On a separate line provide your reasoning for the score as well.
Your job is to judge the relevance and correctness of the generated answer.
Output a single score that represents a holistic evaluation.
You must return your response in a line with only the score.
Do not return answers in any other format.
On a separate line provide your reasoning for the score as well.
Follow these guidelines for scoring:
- Your score has to be between 1 and 5, where 1 is the worst and 5 is the best.
- If the generated answer is not relevant to the user query,
you should give a score of 1.
- If the generated answer is relevant but contains mistakes,
you should give a score between 2 and 3.
- If the generated answer is relevant and fully correct,
you should give a score between 4 and 5.
Follow these guidelines for scoring:
- Your score has to be between 1 and 5, where 1 is the worst and 5 is the best.
- If the generated answer is not relevant to the user query,
you should give a score of 1.
- If the generated answer is relevant but contains mistakes,
you should give a score between 2 and 3.
- If the generated answer is relevant and fully correct,
you should give a score between 4 and 5.
Example Response:
4.0
The generated answer has the exact same metrics as the reference answer
but it is not as concise.
`;
Example Response:
4.0
The generated answer has the exact same metrics as the reference answer
but it is not as concise.
`);
export type CorrectnessSystemPrompt = typeof defaultCorrectnessSystemPrompt;
export const defaultFaithfulnessRefinePrompt = ({
query,
context,
existingAnswer,
}: {
query: string;
context: string;
existingAnswer: string;
}) => `
We want to understand if the following information is present
in the context information: ${query}
We have provided an existing YES/NO answer: ${existingAnswer}
We have the opportunity to refine the existing answer
(only if needed) with some more context below.
------------
${context}
------------
If the existing answer was already YES, still answer YES.
If the information is present in the new context, answer YES.
Otherwise answer NO.
`;
export const defaultFaithfulnessRefinePrompt = new Prompt(`
default: >
We want to understand if the following information is present
in the context information: {{query}}
We have provided an existing YES/NO answer: {{existingAnswer}}
We have the opportunity to refine the existing answer
(only if needed) with some more context below.
------------
{{context}}
------------
If the existing answer was already YES, still answer YES.
If the information is present in the new context, answer YES.
Otherwise answer NO.
`);
export type FaithfulnessRefinePrompt = typeof defaultFaithfulnessRefinePrompt;
export const defaultFaithfulnessTextQaPrompt = ({
query,
context,
}: {
query: string;
context: string;
}) => `
Please tell if a given piece of information
is supported by the context.
You need to answer with either YES or NO.
Answer YES if any of the context supports the information, even
if most of the context is unrelated.
Some examples are provided below.
export const defaultFaithfulnessTextQaPrompt = new Prompt(`
default: >
Please tell if a given piece of information
is supported by the context.
You need to answer with either YES or NO.
Answer YES if any of the context supports the information, even
if most of the context is unrelated.
Some examples are provided below.
Information: Apple pie is generally double-crusted.
Context: An apple pie is a fruit pie in which the principal filling
ingredient is apples.
Apple pie is often served with whipped cream, ice cream
('apple pie à la mode'), custard or cheddar cheese.
It is generally double-crusted, with pastry both above
and below the filling; the upper crust may be solid or
latticed (woven of crosswise strips).
Answer: YES
Information: Apple pies tastes bad.
Context: An apple pie is a fruit pie in which the principal filling
ingredient is apples.
Apple pie is often served with whipped cream, ice cream
('apple pie à la mode'), custard or cheddar cheese.
It is generally double-crusted, with pastry both above
and below the filling; the upper crust may be solid or
latticed (woven of crosswise strips).
Answer: NO
Information: ${query}
Context: ${context}
Answer:
`;
Information: Apple pie is generally double-crusted.
Context: An apple pie is a fruit pie in which the principal filling
ingredient is apples.
Apple pie is often served with whipped cream, ice cream
('apple pie à la mode'), custard or cheddar cheese.
It is generally double-crusted, with pastry both above
and below the filling; the upper crust may be solid or
latticed (woven of crosswise strips).
Answer: YES
Information: Apple pies tastes bad.
Context: An apple pie is a fruit pie in which the principal filling
ingredient is apples.
Apple pie is often served with whipped cream, ice cream
('apple pie à la mode'), custard or cheddar cheese.
It is generally double-crusted, with pastry both above
and below the filling; the upper crust may be solid or
latticed (woven of crosswise strips).
Answer: NO
Information: {{query}}
Context: {{context}}
Answer:
`);
export type FaithfulnessTextQAPrompt = typeof defaultFaithfulnessTextQaPrompt;
export const defaultRelevancyEvalPrompt = ({
query,
context,
}: {
query: string;
context: string;
}) => `Your task is to evaluate if the response for the query is in line with the context information provided.
You have two options to answer. Either YES/ NO.
Answer - YES, if the response for the query is in line with context information otherwise NO.
Query and Response: ${query}
Context: ${context}
Answer: `;
export const defaultRelevancyEvalPrompt = new Prompt(`
default: >
Your task is to evaluate if the response for the query is in line with the context information provided.
You have two options to answer. Either YES/ NO.
Answer - YES, if the response for the query is in line with context information otherwise NO.
Query and Response: {{query}}
Context: {{context}}
Answer:
`);
export type RelevancyEvalPrompt = typeof defaultRelevancyEvalPrompt;
export const defaultRelevancyRefinePrompt = ({
query,
existingAnswer,
contextMsg,
}: {
query: string;
existingAnswer: string;
contextMsg: string;
}) => `We want to understand if the following query and response is
in line with the context information:
${query}
We have provided an existing YES/NO answer:
${existingAnswer}
We have the opportunity to refine the existing answer
(only if needed) with some more context below.
------------
${contextMsg}
------------
If the existing answer was already YES, still answer YES.
If the information is present in the new context, answer YES.
Otherwise answer NO.
`;
export const defaultRelevancyRefinePrompt = new Prompt(`
default: >
We want to understand if the following query and response is
in line with the context information:
{{query}}
We have provided an existing YES/NO answer:
{{existingAnswer}}
We have the opportunity to refine the existing answer
(only if needed) with some more context below.
------------
{{contextMsg}}
------------
If the existing answer was already YES, still answer YES.
If the information is present in the new context, answer YES.
Otherwise answer NO.
`);
export type RelevancyRefinePrompt = typeof defaultRelevancyRefinePrompt;
+1
View File
@@ -8,6 +8,7 @@ export * from "./QuestionGenerator.js";
export * from "./Response.js";
export * from "./Retriever.js";
export * from "./ServiceContext.js";
export * from "./Settings.js";
export * from "./TextSplitter.js";
export * from "./agent/index.js";
export * from "./callbacks/CallbackManager.js";
+1 -1
View File
@@ -354,7 +354,7 @@ export class SummaryIndexLLMRetriever implements BaseRetriever {
const input = { context: fmtBatchStr, query: query };
const rawResponse = (
await this.serviceContext.llm.complete({
prompt: this.choiceSelectPrompt(input),
prompt: this.choiceSelectPrompt.format(input),
})
).text;
+64
View File
@@ -10,11 +10,75 @@ import type {
LLMCompletionParamsStreaming,
LLMMetadata,
} from "./types.js";
import { streamConverter } from "./utils.js";
export abstract class BaseLLM implements LLM {
abstract metadata: LLMMetadata;
predict(
params: LLMCompletionParamsStreaming,
): Promise<AsyncIterable<CompletionResponse>>;
predict(params: LLMCompletionParamsNonStreaming): Promise<CompletionResponse>;
async predict(
params: LLMCompletionParamsStreaming | LLMCompletionParamsNonStreaming,
): Promise<CompletionResponse | AsyncIterable<CompletionResponse>> {
const { prompt, parentEvent, stream } = params;
if (Array.isArray(prompt)) {
if (stream) {
const stream = await this.chat({
messages: prompt,
parentEvent,
stream: true,
});
return streamConverter(stream, (chunk) => {
return {
text: chunk.delta,
};
});
}
const chatResponse = await this.chat({
messages: prompt,
parentEvent,
});
return { text: chatResponse.message.content as string };
}
if (typeof prompt === "string") {
if (stream) {
const stream = await this.complete({
prompt,
parentEvent,
stream: true,
});
return stream;
}
return this.complete({
prompt,
parentEvent,
});
}
if (stream) {
const stream = await this.complete({
prompt,
parentEvent,
stream: true,
});
return stream;
}
return this.complete({
prompt,
parentEvent,
});
}
complete(
params: LLMCompletionParamsStreaming,
): Promise<AsyncIterable<CompletionResponse>>;
+10
View File
@@ -64,6 +64,16 @@ export class Ollama extends BaseEmbedding implements LLM {
};
}
predict(
params: LLMCompletionParamsStreaming,
): Promise<AsyncIterable<CompletionResponse>>;
predict(params: LLMCompletionParamsNonStreaming): Promise<CompletionResponse>;
predict(
params: unknown,
): Promise<AsyncIterable<CompletionResponse>> | Promise<CompletionResponse> {
throw new Error("Method not implemented.");
}
chat(
params: LLMChatParamsStreaming,
): Promise<AsyncIterable<ChatResponseChunk>>;
+9
View File
@@ -6,6 +6,15 @@ import type { Event } from "../callbacks/CallbackManager.js";
*/
export interface LLM {
metadata: LLMMetadata;
/**
* Predict the next completion from the LLM
* *
* @param params
*/
predict(
params: LLMCompletionParamsStreaming,
): Promise<AsyncIterable<CompletionResponse>>;
predict(params: LLMCompletionParamsNonStreaming): Promise<CompletionResponse>;
/**
* Get a chat response from the LLM
*
+2
View File
@@ -1 +1,3 @@
export * from "./Mixin.js";
export * from "./types.js";
export * from "./utils.js";
+104
View File
@@ -0,0 +1,104 @@
import type { ChatMessage } from "../llm/types.js";
import { Settings } from "../Settings.js";
import mustache from "mustache";
import * as yaml from "yaml";
interface Env {
llm?: string;
lang?: string;
}
type Inputs = Record<string, string>;
type FunctionResult = string | ChatMessage[];
interface ParsedTemplate {
[key: string]: any;
}
export class Prompt {
template: string;
constructor(template: string) {
this.template = template;
}
format(inputs: Inputs, env?: Env): FunctionResult {
const parsedTemplate: ParsedTemplate = yaml.parse(this.template);
let templateSection = null;
let defaultSection = null;
// Determine the language and LLM settings to use, either from the environment or the global settings.
const envLang = env?.lang || Settings?.prompt?.lang;
const envLlm = env?.llm || Settings?.prompt?.llm;
// Regular expressions for matching language and LLM keys, including defaults.
const langRegex = new RegExp(`^lang-(${envLang}|default)$`, "i");
const llmRegex = new RegExp(`^llm-(${envLlm}|default)$`, "i");
// First, look for language matches or language defaults.
Object.entries(parsedTemplate).forEach(([key, value]) => {
if (langRegex.test(key)) {
templateSection = value; // Select the matching language section or a default language section.
// Then, within the selected language section, look for LLM matches or LLM defaults.
if (templateSection && typeof templateSection === "object") {
Object.entries(templateSection).forEach(
([nestedKey, nestedValue]) => {
if (llmRegex.test(nestedKey)) {
templateSection = nestedValue; // Further refine to LLM section if available, including defaults.
} else if (/^default$/.test(nestedKey)) {
templateSection = nestedValue;
}
},
);
}
} else if (llmRegex.test(key)) {
templateSection = value;
} else if (/^default$/.test(key)) {
// Keep track of a root-level default to use if no other matches are found.
defaultSection = value;
}
});
// If no specific matches were found, use the root-level default section if available.
if (!templateSection && defaultSection) {
templateSection = defaultSection;
} else if (!templateSection && !defaultSection) {
// If no matches were found, set a single prompt
templateSection = parsedTemplate;
}
// Process the selected template section for message rendering or other logic.
if (templateSection && templateSection["messages"]) {
const result: ChatMessage[] = [];
for (const message of templateSection["messages"] as ChatMessage[]) {
result.push({
content: mustache.render(message.content, inputs),
role: message.role,
});
}
return result;
} else if (typeof templateSection === "string") {
const renderedResult = mustache.render(templateSection, inputs);
return renderedResult;
} else {
throw new Error("Invalid template section");
}
}
partial(inputs: Record<string, string>): Prompt {
const template = mustache.render(this.template, inputs);
return new Prompt(template);
}
toJSON(): Record<string, any> {
return {
template: this.template,
};
}
}
+19
View File
@@ -0,0 +1,19 @@
import type { ChatMessage } from "../llm/types.js";
export const messagesToPrompt = (messages: ChatMessage[]): string => {
const stringMessages = [];
for (const message of messages) {
const role = message.role;
const content = message.content;
let stringMessage = `${role}: ${content}`;
const additionalKwargs = message.additionalKwargs;
if (additionalKwargs) {
stringMessage += `\n${additionalKwargs}`;
}
stringMessages.push(stringMessage);
}
stringMessages.push(`assistant: `);
return stringMessages.join("\n");
};
@@ -6,8 +6,11 @@ import { serviceContextFromDefaults } from "../ServiceContext.js";
import { imageToDataUrl } from "../embeddings/index.js";
import type { MessageContentDetail } from "../llm/types.js";
import { PromptMixin } from "../prompts/Mixin.js";
import type { TextQaPrompt } from "./../Prompt.js";
import { defaultTextQaPrompt } from "./../Prompt.js";
import { Prompt } from "./../prompts/types.js";
import type {
BaseSynthesizer,
SynthesizeParamsNonStreaming,
@@ -20,7 +23,7 @@ export class MultiModalResponseSynthesizer
{
serviceContext: ServiceContext;
metadataMode: MetadataMode;
textQATemplate: TextQaPrompt;
textQATemplate: Prompt;
constructor({
serviceContext,
@@ -34,15 +37,13 @@ export class MultiModalResponseSynthesizer
this.textQATemplate = textQATemplate ?? defaultTextQaPrompt;
}
protected _getPrompts(): { textQATemplate: TextQaPrompt } {
protected _getPrompts(): { textQATemplate: Prompt } {
return {
textQATemplate: this.textQATemplate,
};
}
protected _updatePrompts(promptsDict: {
textQATemplate: TextQaPrompt;
}): void {
protected _updatePrompts(promptsDict: { textQATemplate: Prompt }): void {
if (promptsDict.textQATemplate) {
this.textQATemplate = promptsDict.textQATemplate;
}
@@ -70,7 +71,9 @@ export class MultiModalResponseSynthesizer
);
// TODO: use builders to generate context
const context = textChunks.join("\n\n");
const textPrompt = this.textQATemplate({ context, query });
const textQaPrompt = this.textQATemplate.format({ context, query });
const images = await Promise.all(
imageNodes.map(async (node: ImageNode) => {
return {
@@ -81,11 +84,17 @@ export class MultiModalResponseSynthesizer
} as MessageContentDetail;
}),
);
// TODO: handle chat message prompt
if (Array.isArray(textQaPrompt)) {
throw new Error("textQaPrompt must be a string");
}
const prompt: MessageContentDetail[] = [
{ type: "text", text: textPrompt },
{ type: "text", text: textQaPrompt },
...images,
];
const response = await this.serviceContext.llm.complete({
const response = await this.serviceContext.llm.predict({
prompt,
parentEvent,
});
+53 -62
View File
@@ -1,12 +1,6 @@
import type { Event } from "../callbacks/CallbackManager.js";
import type { LLM } from "../llm/index.js";
import type { ChatMessage, LLM } from "../llm/index.js";
import { streamConverter } from "../llm/utils.js";
import type {
RefinePrompt,
SimplePrompt,
TextQaPrompt,
TreeSummarizePrompt,
} from "../Prompt.js";
import {
defaultRefinePrompt,
defaultTextQaPrompt,
@@ -22,6 +16,8 @@ import type {
ResponseBuilderParamsStreaming,
} from "./types.js";
import { Prompt } from "../prompts/types.js";
/**
* Response modes of the response synthesizer
*/
@@ -37,9 +33,9 @@ enum ResponseMode {
*/
export class SimpleResponseBuilder implements ResponseBuilder {
llm: LLM;
textQATemplate: TextQaPrompt;
textQATemplate: Prompt;
constructor(serviceContext: ServiceContext, textQATemplate?: TextQaPrompt) {
constructor(serviceContext: ServiceContext, textQATemplate?: Prompt) {
this.llm = serviceContext.llm;
this.textQATemplate = textQATemplate ?? defaultTextQaPrompt;
}
@@ -63,7 +59,8 @@ export class SimpleResponseBuilder implements ResponseBuilder {
context: textChunks.join("\n\n"),
};
const prompt = this.textQATemplate(input);
const prompt = this.textQATemplate.format(input);
if (stream) {
const response = await this.llm.complete({ prompt, parentEvent, stream });
return streamConverter(response, (chunk) => chunk.text);
@@ -80,13 +77,13 @@ export class SimpleResponseBuilder implements ResponseBuilder {
export class Refine extends PromptMixin implements ResponseBuilder {
llm: LLM;
promptHelper: PromptHelper;
textQATemplate: TextQaPrompt;
refineTemplate: RefinePrompt;
textQATemplate: Prompt;
refineTemplate: Prompt;
constructor(
serviceContext: ServiceContext,
textQATemplate?: TextQaPrompt,
refineTemplate?: RefinePrompt,
textQATemplate?: Prompt,
refineTemplate?: Prompt,
) {
super();
@@ -97,8 +94,8 @@ export class Refine extends PromptMixin implements ResponseBuilder {
}
protected _getPrompts(): {
textQATemplate: RefinePrompt;
refineTemplate: RefinePrompt;
textQATemplate: Prompt;
refineTemplate: Prompt;
} {
return {
textQATemplate: this.textQATemplate,
@@ -107,8 +104,8 @@ export class Refine extends PromptMixin implements ResponseBuilder {
}
protected _updatePrompts(prompts: {
textQATemplate: RefinePrompt;
refineTemplate: RefinePrompt;
textQATemplate: Prompt;
refineTemplate: Prompt;
}): void {
if (prompts.textQATemplate) {
this.textQATemplate = prompts.textQATemplate;
@@ -166,19 +163,21 @@ export class Refine extends PromptMixin implements ResponseBuilder {
stream: boolean,
parentEvent?: Event,
) {
const textQATemplate: SimplePrompt = (input) =>
this.textQATemplate({ ...input, query: queryStr });
const textChunks = this.promptHelper.repack(textQATemplate, [textChunk]);
const textChunks = this.promptHelper.repack(this.textQATemplate, [
textChunk,
]);
let response: AsyncIterable<string> | string | undefined = undefined;
for (let i = 0; i < textChunks.length; i++) {
const chunk = textChunks[i];
const lastChunk = i === textChunks.length - 1;
if (!response) {
response = await this.complete({
prompt: textQATemplate({
prompt: this.textQATemplate.format({
context: chunk,
query: queryStr,
}),
parentEvent,
stream: stream && lastChunk,
@@ -205,20 +204,21 @@ export class Refine extends PromptMixin implements ResponseBuilder {
stream: boolean,
parentEvent?: Event,
) {
const refineTemplate: SimplePrompt = (input) =>
this.refineTemplate({ ...input, query: queryStr });
const textChunks = this.promptHelper.repack(refineTemplate, [textChunk]);
const textChunks = this.promptHelper.repack(this.refineTemplate, [
textChunk,
]);
let response: AsyncIterable<string> | string = initialReponse;
for (let i = 0; i < textChunks.length; i++) {
const chunk = textChunks[i];
const lastChunk = i === textChunks.length - 1;
response = await this.complete({
prompt: refineTemplate({
prompt: this.refineTemplate.format({
context: chunk,
existingAnswer: response as string,
query: queryStr,
}),
parentEvent,
stream: stream && lastChunk,
@@ -228,15 +228,15 @@ export class Refine extends PromptMixin implements ResponseBuilder {
}
async complete(params: {
prompt: string;
prompt: string | ChatMessage[];
stream: boolean;
parentEvent?: Event;
}): Promise<AsyncIterable<string> | string> {
if (params.stream) {
const response = await this.llm.complete({ ...params, stream: true });
const response = await this.llm.predict({ ...params, stream: true });
return streamConverter(response, (chunk) => chunk.text);
} else {
const response = await this.llm.complete({ ...params, stream: false });
const response = await this.llm.predict({ ...params, stream: false });
return response.text;
}
}
@@ -261,12 +261,10 @@ export class CompactAndRefine extends Refine {
| ResponseBuilderParamsNonStreaming): Promise<
AsyncIterable<string> | string
> {
const textQATemplate: SimplePrompt = (input) =>
this.textQATemplate({ ...input, query: query });
const refineTemplate: SimplePrompt = (input) =>
this.refineTemplate({ ...input, query: query });
const maxPrompt = getBiggestPrompt([textQATemplate, refineTemplate]);
const maxPrompt = getBiggestPrompt([
this.textQATemplate,
this.refineTemplate,
]);
const newTexts = this.promptHelper.repack(maxPrompt, textChunks);
const params = {
query,
@@ -290,12 +288,9 @@ export class CompactAndRefine extends Refine {
export class TreeSummarize extends PromptMixin implements ResponseBuilder {
llm: LLM;
promptHelper: PromptHelper;
summaryTemplate: TreeSummarizePrompt;
summaryTemplate: Prompt;
constructor(
serviceContext: ServiceContext,
summaryTemplate?: TreeSummarizePrompt,
) {
constructor(serviceContext: ServiceContext, summaryTemplate?: Prompt) {
super();
this.llm = serviceContext.llm;
@@ -303,15 +298,13 @@ export class TreeSummarize extends PromptMixin implements ResponseBuilder {
this.summaryTemplate = summaryTemplate ?? defaultTreeSummarizePrompt;
}
protected _getPrompts(): { summaryTemplate: TreeSummarizePrompt } {
protected _getPrompts(): { summaryTemplate: Prompt } {
return {
summaryTemplate: this.summaryTemplate,
};
}
protected _updatePrompts(prompts: {
summaryTemplate: TreeSummarizePrompt;
}): void {
protected _updatePrompts(prompts: { summaryTemplate: Prompt }): void {
if (prompts.summaryTemplate) {
this.summaryTemplate = prompts.summaryTemplate;
}
@@ -342,11 +335,13 @@ export class TreeSummarize extends PromptMixin implements ResponseBuilder {
);
if (packedTextChunks.length === 1) {
const prompt = this.summaryTemplate.partial({
context: packedTextChunks[0],
query,
});
const params = {
prompt: this.summaryTemplate({
context: packedTextChunks[0],
query,
}),
prompt,
parentEvent,
};
if (stream) {
@@ -356,21 +351,20 @@ export class TreeSummarize extends PromptMixin implements ResponseBuilder {
return (await this.llm.complete(params)).text;
} else {
const summaries = await Promise.all(
packedTextChunks.map((chunk) =>
this.llm.complete({
prompt: this.summaryTemplate({
context: chunk,
query,
}),
parentEvent,
}),
),
packedTextChunks.map((chunk) => {
const prompt = this.summaryTemplate.partial({
context: chunk,
query,
});
return this.llm.complete({ prompt, parentEvent });
}),
);
const params = {
query,
textChunks: summaries.map((s) => s.text),
};
if (stream) {
return this.getResponse({
...params,
@@ -398,7 +392,4 @@ export function getResponseBuilder(
}
}
export type ResponseBuilderPrompts =
| TextQaPrompt
| TreeSummarizePrompt
| RefinePrompt;
export type ResponseBuilderPrompts = Prompt | Prompt;
+36
View File
@@ -0,0 +1,36 @@
import { Settings, SimpleDirectoryReader, VectorStoreIndex } from "./index.js";
// Update llm and prompt
Settings.prompt.llm = "claude";
// Update lang
Settings.prompt.lang = "en";
async function main() {
const documents = await new SimpleDirectoryReader().loadData({
directoryPath: "../examples",
});
const index = await VectorStoreIndex.fromDocuments(documents);
const retriever = await index.asRetriever({});
retriever.similarityTopK = 10;
const queryEngine = index.asQueryEngine({
retriever,
});
// Query the engine
const query = "Tell me about abramov";
const response = await queryEngine.query({
query,
});
console.log({
response,
});
}
main();
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "0.0.2",
"type": "module",
"scripts": {
"test": "vitest run"
"test": "vitest run --silent=false"
},
"devDependencies": {
"llamaindex": "workspace:*",
@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import { Prompt } from "llamaindex/prompts/types";
describe("Prompt", () => {
it("should format messages prompt", () => {
const prompt = new Prompt(`
messages:
- content: hello, {{name}}
role: user
`);
const result = prompt.format({ name: "world" });
expect(result).toEqual([{ content: "hello, world", role: "user" }]);
});
it("should format single prompt", () => {
const prompt = new Prompt("hello, {{name}}");
const result = prompt.format({ name: "world" });
expect(result).toEqual("hello, world");
});
it("should format a prompt with a language section", () => {
const prompt = new Prompt(
"lang-en:\n messages:\n - content: 'hello, {{name}}'\n role: user",
);
const result = prompt.format({ name: "world" }, { lang: "en" });
expect(result).toEqual([{ content: "hello, world", role: "user" }]);
});
it("should format a prompt with a language section and LLM section", () => {
const prompt = new Prompt(`
lang-en:
llm-gpt3:
messages:
- content: 'hello, {{name}}'
role: user
`);
const result = prompt.format(
{ name: "world" },
{ lang: "en", llm: "gpt3" },
);
expect(result).toEqual([{ content: "hello, world", role: "user" }]);
});
it("should format a prompt with a default language section and LLM section", () => {
const prompt = new Prompt(`
lang-default:
llm-gpt3:
messages:
- content: hello, {{name}}
role: user
`);
const result = prompt.format({ name: "world" }, { llm: "gpt3" });
expect(result).toEqual([{ content: "hello, world", role: "user" }]);
});
it("should format a prompt with a default language section and default LLM section", () => {
const prompt = new Prompt(`
lang-default:
llm-default:
messages:
- content: hello, {{name}}
role: user
`);
const result = prompt.format({ name: "world" });
expect(result).toEqual([{ content: "hello, world", role: "user" }]);
});
it("should format a default prompt", () => {
const prompt = new Prompt(`
default:
hello, {{name}}
`);
const result = prompt.format({ name: "world" });
expect(result).toEqual("hello, world");
});
});
+3 -1
View File
@@ -29,6 +29,7 @@
"mammoth": "^1.6.0",
"md-utils-ts": "^2.0.0",
"mongodb": "^6.3.0",
"mustache": "^4.2.0",
"notion-md-crawler": "^0.0.2",
"openai": "^4.26.1",
"papaparse": "^5.4.1",
@@ -40,8 +41,9 @@
"rake-modified": "^1.0.8",
"replicate": "^0.25.2",
"string-strip-html": "^13.4.6",
"wikipedia": "^2.1.2",
"wink-nlp": "^1.14.3",
"wikipedia": "^2.1.2"
"yaml": "^2.4.1"
},
"engines": {
"node": ">=18.0.0"
+33 -3
View File
@@ -243,6 +243,9 @@ importers:
mongodb:
specifier: ^6.3.0
version: 6.3.0
mustache:
specifier: ^4.2.0
version: 4.2.0
notion-md-crawler:
specifier: ^0.0.2
version: 0.0.2
@@ -282,6 +285,9 @@ importers:
wink-nlp:
specifier: ^1.14.3
version: 1.14.3
yaml:
specifier: ^2.4.1
version: 2.4.1
devDependencies:
'@swc/cli':
specifier: ^0.3.9
@@ -289,6 +295,9 @@ importers:
'@swc/core':
specifier: ^1.4.2
version: 1.4.2
'@types/mustache':
specifier: ^4.2.5
version: 4.2.5
concurrently:
specifier: ^8.2.2
version: 8.2.2
@@ -388,6 +397,9 @@ importers:
mongodb:
specifier: ^6.3.0
version: 6.3.0
mustache:
specifier: ^4.2.0
version: 4.2.0
notion-md-crawler:
specifier: ^0.0.2
version: 0.0.2
@@ -427,6 +439,9 @@ importers:
wink-nlp:
specifier: ^1.14.3
version: 1.14.3
yaml:
specifier: ^2.4.1
version: 2.4.1
packages/edge/e2e/test-edge-runtime:
dependencies:
@@ -2419,7 +2434,7 @@ packages:
'@docusaurus/react-loadable': 5.5.2(react@18.2.0)
'@docusaurus/types': 3.1.1(react-dom@18.2.0)(react@18.2.0)
'@types/history': 4.7.11
'@types/react': 18.2.65
'@types/react': 18.2.66
'@types/react-router-config': 5.0.11
'@types/react-router-dom': 5.3.3
react: 18.2.0
@@ -4574,6 +4589,10 @@ packages:
/@types/ms@0.7.34:
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
/@types/mustache@4.2.5:
resolution: {integrity: sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==}
dev: true
/@types/node-fetch@2.6.11:
resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
dependencies:
@@ -4668,7 +4687,7 @@ packages:
resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==}
dependencies:
'@types/history': 4.7.11
'@types/react': 18.2.65
'@types/react': 18.2.66
'@types/react-router': 5.1.20
dev: true
@@ -4692,7 +4711,7 @@ packages:
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
dependencies:
'@types/history': 4.7.11
'@types/react': 18.2.65
'@types/react': 18.2.66
dev: true
/@types/react@18.2.48:
@@ -11080,6 +11099,11 @@ packages:
dns-packet: 5.6.1
thunky: 1.1.0
/mustache@4.2.0:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
hasBin: true
dev: false
/mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
dependencies:
@@ -15836,6 +15860,12 @@ packages:
engines: {node: '>= 14'}
dev: true
/yaml@2.4.1:
resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==}
engines: {node: '>= 14'}
hasBin: true
dev: false
/yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}