Compare commits

...

40 Commits

Author SHA1 Message Date
Thuc Pham e10ca4a216 add handle llm tool call step 2024-09-23 21:17:48 +07:00
Thuc Pham 94c623ecfb check toolcall support 2024-09-23 17:45:20 +07:00
Thuc Pham 0ec268cd7f fix: stream response 2024-09-23 17:13:53 +07:00
Thuc Pham 4e6a04ba62 refactor: remove dup code in handleLLMInput 2024-09-23 17:03:23 +07:00
Thuc Pham 891d9fbe65 patch controller express template 2024-09-23 16:41:39 +07:00
Thuc Pham f053da4728 refactor: rename workflow files 2024-09-23 16:41:28 +07:00
Thuc Pham e01ad418e5 fix: lint 2024-09-23 16:25:55 +07:00
Thuc Pham 97eb4dc51b feat: support ts multi-agent 2024-09-23 14:15:01 +07:00
Marcus Schiesser 0bf11a57b0 Update questions.ts 2024-09-23 11:05:51 +07:00
Thuc Pham f7d366b648 feat: support multiagent for nextjs 2024-09-20 17:13:19 +07:00
Thuc Pham d69cd42fa7 refactor: move workflow to components 2024-09-20 17:00:43 +07:00
Thuc Pham 54d74f8237 fix: move settings.ts to setting folder 2024-09-20 15:56:05 +07:00
Thuc Pham f6597213c8 refactor: share settings file for ts templates 2024-09-20 15:44:25 +07:00
Thuc Pham c4041e2de3 refactor: move workflow folder to src 2024-09-20 15:33:23 +07:00
Thuc Pham aff4f0cde4 fix lint 2024-09-20 15:28:51 +07:00
Thuc Pham de5ba29276 fix: let default max attempt 2 2024-09-20 15:28:19 +07:00
Thuc Pham 33ce5934fa feat: funtional calling agent 2024-09-20 15:23:54 +07:00
Thuc Pham b030a3d885 fix: pipe final streaming result 2024-09-19 19:59:54 +07:00
Thuc Pham b8756189cc fix: streaming final result 2024-09-19 19:32:16 +07:00
Thuc Pham 5daf519572 feat: streaming event 2024-09-19 16:58:41 +07:00
Thuc Pham 2c7a53853a update doc 2024-09-19 15:44:46 +07:00
Thuc Pham 6c05872aae remove unused files 2024-09-19 15:44:40 +07:00
Thuc Pham f43f00a4ee create workflow with example agents 2024-09-19 15:05:20 +07:00
Marcus Schiesser 0ebcb9fff7 Create yellow-jokes-protect.md 2024-09-19 09:26:48 +07:00
Thuc Pham f464b40f58 fix: import from agent 2024-09-18 19:29:15 +07:00
Thuc Pham 622b84b97a feat: add express simple multiagent 2024-09-18 19:26:23 +07:00
Thuc Pham 413593b0d9 feat: update question to ask creating multiagent in express 2024-09-18 19:26:02 +07:00
Thuc Pham 8c1087f5f1 feat: enhance style for markdown (#298) 2024-09-18 11:37:56 +07:00
Huu Le 27333973f1 fixed llama-index-core with 0.11.9 (#296) 2024-09-18 11:26:43 +07:00
Marcus Schiesser cf3ec97a4c Dynamically select model for Groq (#278)
---------
Co-authored-by: Jac-Zac <jacopozac@icloud.com>
Co-authored-by: Thuc Pham <51660321+thucpn@users.noreply.github.com>
2024-09-18 09:29:10 +07:00
Thuc Pham 505b8e944a bump: use latest ai package version (#292) 2024-09-16 17:49:58 +07:00
github-actions[bot] 578f7f9e50 Release 0.2.6 (#288)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-13 18:58:55 +07:00
Thuc Pham adc40cf770 fix: vercel ai update crash sending annotations (#287)
* fix: vercel ai update crash sending annotations

* Create five-ties-happen.md
2024-09-13 18:55:46 +07:00
github-actions[bot] 7bce7386d5 Release 0.2.5 (#285)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-12 13:53:28 +07:00
Huu Le c011455dc4 fix cannot upload file (#286) 2024-09-12 13:51:48 +07:00
Thuc Pham 38a8be8d12 fix: filter in mongo vector store (#269) 2024-09-12 11:34:54 +07:00
github-actions[bot] 6e70eb4d11 Release 0.2.4 (#284)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-10 10:32:14 +07:00
Huu Le 917e862202 chore: fix ts syntax (#283) 2024-09-10 10:17:29 +07:00
github-actions[bot] e363bfeecc Release 0.2.3 (#281)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-09 17:18:40 +07:00
Huu Le b6da3c2419 chore: Always use file loader as default loader (#279) 2024-09-09 17:07:04 +07:00
32 changed files with 887 additions and 305 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
bump: use latest ai package version
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Dynamically select model for Groq
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
feat: enhance style for markdown
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Add multi agents template for Express
+24
View File
@@ -1,5 +1,29 @@
# create-llama
## 0.2.6
### Patch Changes
- adc40cf: fix: vercel ai update crash sending annotations
## 0.2.5
### Patch Changes
- 38a8be8: fix: filter in mongo vector store
## 0.2.4
### Patch Changes
- 917e862: Fix errors in building the frontend
## 0.2.3
### Patch Changes
- b6da3c2: Ensure the generation script always works
## 0.2.2
### Patch Changes
+52 -60
View File
@@ -36,74 +36,66 @@ export async function writeLoadersConfig(
dataSources: TemplateDataSource[],
useLlamaParse?: boolean,
) {
if (dataSources.length === 0) return; // no datasources, no config needed
const loaderConfig = new Document({});
// Web loader config
const loaderConfig: Record<string, any> = {};
// Always set file loader config
loaderConfig.file = createFileLoaderConfig(useLlamaParse);
if (dataSources.some((ds) => ds.type === "web")) {
const webLoaderConfig = new Document({});
// Create config for browser driver arguments
const driverArgNodeValue = webLoaderConfig.createNode([
"--no-sandbox",
"--disable-dev-shm-usage",
]);
driverArgNodeValue.commentBefore =
" The arguments to pass to the webdriver. E.g.: add --headless to run in headless mode";
webLoaderConfig.set("driver_arguments", driverArgNodeValue);
// Create config for urls
const urlConfigs = dataSources
.filter((ds) => ds.type === "web")
.map((ds) => {
const dsConfig = ds.config as WebSourceConfig;
return {
base_url: dsConfig.baseUrl,
prefix: dsConfig.prefix,
depth: dsConfig.depth,
};
});
const urlConfigNode = webLoaderConfig.createNode(urlConfigs);
urlConfigNode.commentBefore = ` base_url: The URL to start crawling with
prefix: Only crawl URLs matching the specified prefix
depth: The maximum depth for BFS traversal
You can add more websites by adding more entries (don't forget the - prefix from YAML)`;
webLoaderConfig.set("urls", urlConfigNode);
// Add web config to the loaders config
loaderConfig.set("web", webLoaderConfig);
loaderConfig.web = createWebLoaderConfig(dataSources);
}
// File loader config
if (dataSources.some((ds) => ds.type === "file")) {
// Add documentation to web loader config
const node = loaderConfig.createNode({
use_llama_parse: useLlamaParse,
});
node.commentBefore = ` use_llama_parse: Use LlamaParse if \`true\`. Needs a \`LLAMA_CLOUD_API_KEY\` from https://cloud.llamaindex.ai set as environment variable`;
loaderConfig.set("file", node);
}
// DB loader config
const dbLoaders = dataSources.filter((ds) => ds.type === "db");
if (dbLoaders.length > 0) {
const dbLoaderConfig = new Document({});
const configEntries = dbLoaders.map((ds) => {
const dsConfig = ds.config as DbSourceConfig;
return {
uri: dsConfig.uri,
queries: [dsConfig.queries],
};
});
const node = dbLoaderConfig.createNode(configEntries);
node.commentBefore = ` The configuration for the database loader, only supports MySQL and PostgreSQL databases for now.
uri: The URI for the database. E.g.: mysql+pymysql://user:password@localhost:3306/db or postgresql+psycopg2://user:password@localhost:5432/db
query: The query to fetch data from the database. E.g.: SELECT * FROM table`;
loaderConfig.set("db", node);
loaderConfig.db = createDbLoaderConfig(dbLoaders);
}
// Create a new Document with the loaderConfig
const yamlDoc = new Document(loaderConfig);
// Write loaders config
const loaderConfigPath = path.join(root, "config", "loaders.yaml");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(loaderConfigPath, yaml.stringify(loaderConfig));
await fs.writeFile(loaderConfigPath, yaml.stringify(yamlDoc));
}
function createWebLoaderConfig(dataSources: TemplateDataSource[]): any {
const webLoaderConfig: Record<string, any> = {};
// Create config for browser driver arguments
webLoaderConfig.driver_arguments = [
"--no-sandbox",
"--disable-dev-shm-usage",
];
// Create config for urls
const urlConfigs = dataSources
.filter((ds) => ds.type === "web")
.map((ds) => {
const dsConfig = ds.config as WebSourceConfig;
return {
base_url: dsConfig.baseUrl,
prefix: dsConfig.prefix,
depth: dsConfig.depth,
};
});
webLoaderConfig.urls = urlConfigs;
return webLoaderConfig;
}
function createFileLoaderConfig(useLlamaParse?: boolean): any {
return {
use_llama_parse: useLlamaParse,
};
}
function createDbLoaderConfig(dbLoaders: TemplateDataSource[]): any {
return dbLoaders.map((ds) => {
const dsConfig = ds.config as DbSourceConfig;
return {
uri: dsConfig.uri,
queries: [dsConfig.queries],
};
});
}
+19 -18
View File
@@ -96,10 +96,11 @@ async function generateContextData(
}
}
const copyContextData = async (
const prepareContextData = async (
root: string,
dataSources: TemplateDataSource[],
) => {
await makeDir(path.join(root, "data"));
for (const dataSource of dataSources) {
const dataSourceConfig = dataSource?.config as FileSourceConfig;
// Copy local data
@@ -174,25 +175,25 @@ export const installTemplate = async (
await createBackendEnvFile(props.root, props);
}
if (props.dataSources.length > 0) {
await prepareContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
);
if (
props.dataSources.length > 0 &&
(props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies")
) {
console.log("\nGenerating context data...\n");
await copyContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
await generateContextData(
props.framework,
props.modelConfig,
props.packageManager,
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
);
if (
props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies"
) {
await generateContextData(
props.framework,
props.modelConfig,
props.packageManager,
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
);
}
}
// Create outputs directory
+52 -3
View File
@@ -3,8 +3,55 @@ import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions";
const MODELS = ["llama3-8b", "llama3-70b", "mixtral-8x7b"];
const DEFAULT_MODEL = MODELS[0];
import got from "got";
import ora from "ora";
import { red } from "picocolors";
const GROQ_API_URL = "https://api.groq.com/openai/v1";
async function getAvailableModelChoicesGroq(apiKey: string) {
if (!apiKey) {
throw new Error("Need Groq API key to retrieve model choices");
}
const spinner = ora("Fetching available models from Groq").start();
try {
const response = await got(`${GROQ_API_URL}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
timeout: 5000,
responseType: "json",
});
const data: any = await response.body;
spinner.stop();
// Filter out the Whisper models
return data.data
.filter((model: any) => !model.id.toLowerCase().includes("whisper"))
.map((el: any) => {
return {
title: el.id,
value: el.id,
};
});
} catch (error: unknown) {
spinner.stop();
console.log(error);
if ((error as any).response?.statusCode === 401) {
console.log(
red(
"Invalid Groq API key provided! Please provide a valid key and try again!",
),
);
} else {
console.log(red("Request failed: " + error));
}
process.exit(1);
}
}
const DEFAULT_MODEL = "llama3-70b-8192";
// Use huggingface embedding models for now as Groq doesn't support embedding models
enum HuggingFaceEmbeddingModelType {
@@ -66,12 +113,14 @@ export async function askGroqQuestions({
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
choices: modelChoices,
initial: 0,
},
questionHandlers,
+30 -2
View File
@@ -33,8 +33,7 @@ export const installTSTemplate = async ({
* Copy the template files to the target directory.
*/
console.log("\nInitializing project with template:", template, "\n");
const type = template === "multiagent" ? "streaming" : template; // use nextjs streaming template for multiagent
const templatePath = path.join(templatesDir, "types", type, framework);
const templatePath = path.join(templatesDir, "types", "streaming", framework);
const copySource = ["**"];
await copy(copySource, root, {
@@ -124,6 +123,30 @@ export const installTSTemplate = async ({
cwd: path.join(compPath, "vectordbs", "typescript", vectorDb ?? "none"),
});
if (template === "multiagent") {
const multiagentPath = path.join(compPath, "multiagent", "typescript");
// copy workflow code for multiagent template
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
parents: true,
cwd: path.join(multiagentPath, "workflow"),
});
if (framework === "nextjs") {
// patch route.ts file
await copy("**", path.join(root, relativeEngineDestPath), {
parents: true,
cwd: path.join(multiagentPath, "nextjs"),
});
} else if (framework === "express") {
// patch chat.controller.ts file
await copy("**", path.join(root, relativeEngineDestPath), {
parents: true,
cwd: path.join(multiagentPath, "express"),
});
}
}
// copy loader component (TS only supports llama_parse and file for now)
const loaderFolder = useLlamaParse ? "llama_parse" : "file";
await copy("**", enginePath, {
@@ -145,6 +168,11 @@ export const installTSTemplate = async ({
cwd: path.join(compPath, "engines", "typescript", engine),
});
// copy settings to engine folder
await copy("**", enginePath, {
cwd: path.join(compPath, "settings", "typescript"),
});
/**
* Copy the selected UI files to the target directory and reference it.
*/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.2.2",
"version": "0.2.6",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
+2 -4
View File
@@ -410,10 +410,7 @@ export const askQuestions = async (
return; // early return - no further questions needed for llamapack projects
}
if (program.template === "multiagent") {
// TODO: multi-agents currently only supports FastAPI
program.framework = preferences.framework = "fastapi";
} else if (program.template === "extractor") {
if (program.template === "extractor") {
// Extractor template only supports FastAPI, empty data sources, and llamacloud
// So we just use example file for extractor template, this allows user to choose vector database later
program.dataSources = [EXAMPLE_FILE];
@@ -637,6 +634,7 @@ export const askQuestions = async (
type: "db",
config: await prompts(dbPrompts, questionHandlers),
});
break;
}
case "llamacloud": {
program.dataSources.push({
@@ -0,0 +1,34 @@
import { Message, StreamData, streamToResponse } from "ai";
import { Request, Response } from "express";
import { ChatMessage } from "llamaindex";
import { createStreamTimeout } from "./llamaindex/streaming/events";
import { createWorkflow } from "./workflow/factory";
import { toDataStream } from "./workflow/stream";
export const chat = async (req: Request, res: Response) => {
const vercelStreamData = new StreamData();
const streamTimeout = createStreamTimeout(vercelStreamData);
try {
const { messages }: { messages: Message[] } = req.body;
const userMessage = messages.pop();
if (!messages || !userMessage || userMessage.role !== "user") {
return res.status(400).json({
error:
"messages are required in the request body and the last message must be from the user",
});
}
const chatHistory = messages as ChatMessage[];
const agent = await createWorkflow(chatHistory, vercelStreamData);
agent.run(userMessage.content);
const stream = toDataStream(agent.streamEvents(), vercelStreamData);
return streamToResponse(stream, res, {}, vercelStreamData);
} catch (error) {
console.error("[LlamaIndex]", error);
return res.status(500).json({
detail: (error as Error).message,
});
} finally {
clearTimeout(streamTimeout);
}
};
@@ -0,0 +1,53 @@
import { initObservability } from "@/app/observability";
import { Message, StreamData, StreamingTextResponse } from "ai";
import { ChatMessage } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
import { initSettings } from "./engine/settings";
import { createStreamTimeout } from "./llamaindex/streaming/events";
import { createWorkflow } from "./workflow/factory";
import { toDataStream } from "./workflow/stream";
initObservability();
initSettings();
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) {
// Init Vercel AI StreamData and timeout
const vercelStreamData = new StreamData();
const streamTimeout = createStreamTimeout(vercelStreamData);
try {
const body = await request.json();
const { messages, data }: { messages: Message[]; data?: any } = body;
const userMessage = messages.pop();
if (!messages || !userMessage || userMessage.role !== "user") {
return NextResponse.json(
{
error:
"messages are required in the request body and the last message must be from the user",
},
{ status: 400 },
);
}
const chatHistory = messages as ChatMessage[];
const agent = await createWorkflow(chatHistory, vercelStreamData);
agent.run(userMessage.content);
const stream = toDataStream(agent.streamEvents(), vercelStreamData);
return new StreamingTextResponse(stream, {}, vercelStreamData);
} catch (error) {
console.error("[LlamaIndex]", error);
return NextResponse.json(
{
detail: (error as Error).message,
},
{
status: 500,
},
);
} finally {
clearTimeout(streamTimeout);
}
}
@@ -0,0 +1,49 @@
import { ChatMessage, QueryEngineTool } from "llamaindex";
import { getDataSource } from "../engine";
import { FunctionCallingAgent } from "./single-agent";
const getQueryEngineTool = async () => {
const index = await getDataSource();
if (!index) {
throw new Error("Index not found. Please create an index first.");
}
const topK = process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined;
return new QueryEngineTool({
queryEngine: index.asQueryEngine({
similarityTopK: topK,
}),
metadata: {
name: "query_index",
description: `Use this tool to retrieve information about the text corpus from the index.`,
},
});
};
export const createResearcher = async (chatHistory: ChatMessage[]) => {
return new FunctionCallingAgent({
name: "researcher",
tools: [await getQueryEngineTool()],
systemPrompt:
"You are a researcher agent. You are given a researching task. You must use your tools to complete the research.",
chatHistory,
});
};
export const createWriter = (chatHistory: ChatMessage[]) => {
return new FunctionCallingAgent({
name: "writer",
systemPrompt:
"You are an expert in writing blog posts. You are given a task to write a blog post. Don't make up any information yourself.",
chatHistory,
});
};
export const createReviewer = (chatHistory: ChatMessage[]) => {
return new FunctionCallingAgent({
name: "reviewer",
systemPrompt:
"You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement. Furthermore, proofread the post for grammar and spelling errors. Only if the post is good enough for publishing, then you MUST return 'The post is good.'. In all other cases return your review.",
chatHistory,
});
};
@@ -0,0 +1,138 @@
import {
Context,
StartEvent,
StopEvent,
Workflow,
WorkflowEvent,
} from "@llamaindex/core/workflow";
import { StreamData } from "ai";
import { ChatMessage, ChatResponseChunk } from "llamaindex";
import { createResearcher, createReviewer, createWriter } from "./agents";
import { AgentInput, AgentRunEvent, AgentRunResult } from "./type";
const TIMEOUT = 360 * 1000;
const MAX_ATTEMPTS = 2;
class ResearchEvent extends WorkflowEvent<{ input: string }> {}
class WriteEvent extends WorkflowEvent<{
input: string;
isGood: boolean;
}> {}
class ReviewEvent extends WorkflowEvent<{ input: string }> {}
export const createWorkflow = async (
chatHistory: ChatMessage[],
stream: StreamData,
) => {
const appendStream = (agent: string, text: string) => {
stream.appendMessageAnnotation({
type: "agent",
data: { agent, text },
});
};
const runAgent = async (agent: Workflow, input: AgentInput) => {
const run = agent.run(new StartEvent({ input }));
for await (const event of agent.streamEvents()) {
if (event.data instanceof AgentRunEvent) {
const { name, msg } = event.data.data;
// TODO: better using context.writeEventToStream here instead of directly append to stream
// But not sure why it's fail to write to stream from the third event
appendStream(name, msg);
}
}
return await run;
};
const start = async (context: Context, ev: StartEvent) => {
context.set("task", ev.data.input);
return new ResearchEvent({
input: `Research for this task: ${ev.data.input}`,
});
};
const research = async (context: Context, ev: ResearchEvent) => {
const researcher = await createResearcher(chatHistory);
const researchRes = await runAgent(researcher, { message: ev.data.input });
const researchResult = researchRes.data.result;
return new WriteEvent({
input: `Write a blog post given this task: ${context.get("task")} using this research content: ${researchResult}`,
isGood: false,
});
};
const write = async (context: Context, ev: WriteEvent) => {
context.set("attempts", context.get("attempts", 0) + 1);
const tooManyAttempts = context.get("attempts") > MAX_ATTEMPTS;
if (tooManyAttempts) {
appendStream(
"writer",
`Too many attempts (${MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.`,
);
}
if (ev.data.isGood || tooManyAttempts) {
const writer = createWriter(chatHistory);
const writeRes = (await runAgent(writer, {
message: ev.data.input,
streaming: true,
})) as unknown as StopEvent<AsyncGenerator<ChatResponseChunk>>;
const result = writeRes.data.result;
context.writeEventToStream({
data: new AgentRunResult(result),
});
return new StopEvent({ result }); // stop the workflow
}
const writer = createWriter(chatHistory);
const writeRes = await runAgent(writer, { message: ev.data.input });
const writeResult = writeRes.data.result;
context.set("result", writeResult); // store the last result
return new ReviewEvent({ input: writeResult });
};
const review = async (context: Context, ev: ReviewEvent) => {
const reviewer = createReviewer(chatHistory);
const reviewRes = await reviewer.run(
new StartEvent<AgentInput>({ input: { message: ev.data.input } }),
);
const reviewResult = reviewRes.data.result;
const oldContent = context.get("result");
const postIsGood = reviewResult.toLowerCase().includes("post is good");
appendStream(
"reviewer",
`The post is ${postIsGood ? "" : "not "}good enough for publishing. Sending back to the writer${
postIsGood ? " for publication." : "."
}`,
);
if (postIsGood) {
return new WriteEvent({
input: `You're blog post is ready for publication. Please respond with just the blog post. Blog post: \`\`\`${oldContent}\`\`\``,
isGood: true,
});
}
return new WriteEvent({
input: `Improve the writing of a given blog post by using a given review.
Blog post:
\`\`\`
${oldContent}
\`\`\`
Review:
\`\`\`
${reviewResult}
\`\`\``,
isGood: false,
});
};
const workflow = new Workflow({ timeout: TIMEOUT, validate: true });
workflow.addStep(StartEvent, start, { outputs: ResearchEvent });
workflow.addStep(ResearchEvent, research, { outputs: WriteEvent });
workflow.addStep(WriteEvent, write, { outputs: [ReviewEvent, StopEvent] });
workflow.addStep(ReviewEvent, review, { outputs: WriteEvent });
return workflow;
};
@@ -0,0 +1,252 @@
import {
Context,
StartEvent,
StopEvent,
Workflow,
WorkflowEvent,
} from "@llamaindex/core/workflow";
import {
BaseToolWithCall,
ChatMemoryBuffer,
ChatMessage,
ChatResponse,
ChatResponseChunk,
LLM,
Settings,
ToolCall,
ToolCallLLM,
} from "llamaindex";
import { AgentInput, AgentRunEvent } from "./type";
class InputEvent extends WorkflowEvent<{
input: ChatMessage[];
}> {}
class ToolCallEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
export class FunctionCallingAgent extends Workflow {
name: string;
llm: LLM;
memory: ChatMemoryBuffer;
tools: BaseToolWithCall[];
systemPrompt?: string;
writeEvents: boolean;
role?: string;
toolCalled: boolean = false;
constructor(options: {
name: string;
llm?: LLM;
chatHistory?: ChatMessage[];
tools?: BaseToolWithCall[];
systemPrompt?: string;
writeEvents?: boolean;
role?: string;
verbose?: boolean;
timeout?: number;
}) {
super({
verbose: options?.verbose ?? false,
timeout: options?.timeout ?? 360,
});
this.name = options?.name;
this.llm = options.llm ?? Settings.llm;
this.checkToolCallSupport();
this.memory = new ChatMemoryBuffer({
llm: this.llm,
chatHistory: options.chatHistory,
});
this.tools = options?.tools ?? [];
this.systemPrompt = options.systemPrompt;
this.writeEvents = options?.writeEvents ?? true;
this.role = options?.role;
// add steps
this.addStep(StartEvent<AgentInput>, this.prepareChatHistory, {
outputs: InputEvent,
});
this.addStep(InputEvent, this.handleLLMInput, {
outputs: [ToolCallEvent, StopEvent],
});
this.addStep(ToolCallEvent, this.handleToolCalls, {
outputs: InputEvent,
});
}
private get chatHistory() {
return this.memory.getAllMessages();
}
private get toolsByName() {
return this.tools.reduce((acc: Record<string, BaseToolWithCall>, tool) => {
acc[tool.metadata.name] = tool;
return acc;
}, {});
}
private async prepareChatHistory(
ctx: Context,
ev: StartEvent<AgentInput>,
): Promise<InputEvent> {
this.toolCalled = false;
const { message, streaming } = ev.data.input;
ctx.set("streaming", streaming);
this.writeEvent(`Start to work on: ${message}`, ctx);
if (this.systemPrompt) {
this.memory.put({ role: "system", content: this.systemPrompt });
}
this.memory.put({ role: "user", content: message });
return new InputEvent({ input: this.chatHistory });
}
private async handleLLMInput(
ctx: Context,
ev: InputEvent,
): Promise<StopEvent<string | AsyncGenerator> | ToolCallEvent> {
const isStreaming = ctx.get("streaming");
const llmArgs = { messages: this.chatHistory, tools: this.tools };
if (isStreaming) {
return await this.handleLLMInputStream(ctx, ev);
}
const nonStreamingRes = await this.llm.chat({ ...llmArgs });
const toolCalls = this.getToolCallsFromResponse(nonStreamingRes);
if (toolCalls.length && !this.toolCalled) {
return new ToolCallEvent({ toolCalls });
}
this.writeEvent("Finished task", ctx);
const result = nonStreamingRes.message.content.toString();
return new StopEvent({ result });
}
private async handleLLMInputStream(
context: Context,
ev: InputEvent,
): Promise<StopEvent<AsyncGenerator> | ToolCallEvent> {
const { llm, tools, memory } = this;
const llmArgs = { messages: this.chatHistory, tools };
const responseGenerator = async function* () {
const responseStream = await llm.chat({ ...llmArgs, stream: true });
let fullResponse = null;
let yieldedIndicator = false;
for await (const chunk of responseStream) {
const hasToolCalls = chunk.options && "toolCall" in chunk.options;
if (!hasToolCalls) {
if (!yieldedIndicator) {
yield false;
yieldedIndicator = true;
}
yield chunk;
} else if (!yieldedIndicator) {
yield true;
yieldedIndicator = true;
}
fullResponse = chunk;
}
if (fullResponse) {
memory.put({
role: "system",
content: fullResponse.delta,
});
yield fullResponse;
}
};
const generator = responseGenerator();
const isToolCall = await generator.next();
if (isToolCall.value) {
const fullResponse = await generator.next();
const toolCalls = this.getToolCallsFromResponse(
fullResponse.value as ChatResponseChunk<object>,
);
return new ToolCallEvent({ toolCalls });
}
this.writeEvent("Finished task", context);
return new StopEvent({ result: generator });
}
private async handleToolCalls(
ctx: Context,
ev: ToolCallEvent,
): Promise<InputEvent> {
this.toolCalled = true;
const { toolCalls } = ev.data;
const toolMsgs: ChatMessage[] = [];
for (const toolCall of toolCalls) {
const tool = this.toolsByName[toolCall.name];
const options = {
tool_call_id: toolCall.id,
name: tool.metadata.name,
};
if (!tool) {
toolMsgs.push({
role: "system",
content: `Tool ${toolCall.name} does not exist`,
options,
});
continue;
}
try {
const toolInput = JSON.parse(toolCall.input.toString());
const toolOutput = await tool.call(toolInput);
toolMsgs.push({
role: "system",
content: toolOutput.toString(),
options,
});
} catch (e) {
console.error(e);
toolMsgs.push({
role: "system",
content: `Encountered error in tool call: ${e}`,
options,
});
}
}
for (const msg of toolMsgs) {
this.memory.put(msg);
}
return new InputEvent({ input: this.memory.getAllMessages() });
}
private writeEvent(msg: string, context: Context) {
if (!this.writeEvents) return;
context.writeEventToStream({
data: new AgentRunEvent({ name: this.name, msg }),
});
}
private checkToolCallSupport() {
const { supportToolCall } = this.llm as ToolCallLLM;
if (!supportToolCall) throw new Error("LLM does not support tool calls");
}
// TODO: in LITS, llm should have a method to get tool calls from response
// then we don't need to use toolCalled flag
private getToolCallsFromResponse(
response: ChatResponse<object> | ChatResponseChunk<object>,
): ToolCall[] {
let options;
if ("message" in response) {
options = response.message.options;
} else {
options = response.options;
}
if (options && "toolCall" in options) {
return options.toolCall as ToolCall[];
}
return [];
}
}
@@ -0,0 +1,44 @@
import { WorkflowEvent } from "@llamaindex/core/workflow";
import {
createCallbacksTransformer,
createStreamDataTransformer,
StreamData,
trimStartOfStreamHelper,
type AIStreamCallbacksAndOptions,
} from "ai";
import { AgentRunResult } from "./type";
export function toDataStream(
generator: AsyncGenerator<WorkflowEvent, void>,
data: StreamData,
callbacks?: AIStreamCallbacksAndOptions,
) {
return toReadableStream(generator, data)
.pipeThrough(createCallbacksTransformer(callbacks))
.pipeThrough(createStreamDataTransformer());
}
function toReadableStream(
generator: AsyncGenerator<WorkflowEvent, void>,
data: StreamData,
) {
const trimStartOfStream = trimStartOfStreamHelper();
return new ReadableStream<string>({
start(controller) {
controller.enqueue(""); // Kickstart the stream
},
async pull(controller): Promise<void> {
const { value, done } = await generator.next();
if (done) return;
if (value.data instanceof AgentRunResult) {
const finalResultStream = value.data.response;
for await (const event of finalResultStream) {
const text = trimStartOfStream(event.delta ?? "");
if (text) controller.enqueue(text);
}
controller.close();
data.close();
}
},
});
}
@@ -0,0 +1,18 @@
import { WorkflowEvent } from "@llamaindex/core/workflow";
import { ChatResponseChunk } from "llamaindex";
export type AgentInput = {
message: string;
streaming?: boolean;
};
export class AgentRunEvent extends WorkflowEvent<{
name: string;
msg: string;
}> {}
export class AgentRunResult {
constructor(
public response: AsyncGenerator<ChatResponseChunk, any, unknown>,
) {}
}
+7 -2
View File
@@ -3,7 +3,7 @@ import mimetypes
import os
from io import BytesIO
from pathlib import Path
from typing import Any, List, Tuple
from typing import List, Optional, Tuple
from app.engine.index import IndexConfig, get_index
from llama_index.core import VectorStoreIndex
@@ -72,7 +72,12 @@ class PrivateFileService:
return documents
@staticmethod
def process_file(file_name: str, base64_content: str, params: Any) -> List[str]:
def process_file(
file_name: str, base64_content: str, params: Optional[dict] = None
) -> List[str]:
if params is None:
params = {}
file_data, extension = PrivateFileService.preprocess_base64_file(base64_content)
# Add the nodes to the index and persist it
@@ -126,13 +126,7 @@ def init_fastembed():
def init_groq():
from llama_index.llms.groq import Groq
model_map: Dict[str, str] = {
"llama3-8b": "llama3-8b-8192",
"llama3-70b": "llama3-70b-8192",
"mixtral-8x7b": "mixtral-8x7b-32768",
}
Settings.llm = Groq(model=model_map[os.getenv("MODEL")])
Settings.llm = Groq(model=os.getenv("MODEL"))
# Groq does not provide embeddings, so we use FastEmbed instead
init_fastembed()
@@ -138,14 +138,8 @@ function initGroq() {
"all-mpnet-base-v2": "Xenova/all-mpnet-base-v2",
};
const modelMap: Record<string, string> = {
"llama3-8b": "llama3-8b-8192",
"llama3-70b": "llama3-70b-8192",
"mixtral-8x7b": "mixtral-8x7b-32768",
};
Settings.llm = new Groq({
model: modelMap[process.env.MODEL!],
model: process.env.MODEL!,
});
Settings.embedModel = new HuggingFaceEmbedding({
@@ -1,14 +1,11 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import {
MongoDBAtlasVectorSearch,
VectorStoreIndex,
storageContextFromDefaults,
} from "llamaindex";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
import { MongoDBAtlasVectorSearch } from "llamaindex/storage/vectorStore/MongoDBAtlasVectorStore";
import { MongoClient } from "mongodb";
import { getDocuments } from "./loader";
import { initSettings } from "./settings";
import { checkRequiredEnvVars } from "./shared";
import { checkRequiredEnvVars, POPULATED_METADATA_FIELDS } from "./shared";
dotenv.config();
@@ -30,6 +27,12 @@ async function loadAndIndex() {
dbName: databaseName,
collectionName: vectorCollectionName, // this is where your embeddings will be stored
indexName: indexName, // this is the name of the index you will need to create
indexedMetadataFields: POPULATED_METADATA_FIELDS,
embeddingDefinition: {
dimensions: process.env.EMBEDDING_DIM
? parseInt(process.env.EMBEDDING_DIM)
: 1536,
},
});
// now create an index from all the Documents and store them in Atlas
@@ -1,16 +1,23 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MongoDBAtlasVectorSearch, VectorStoreIndex } from "llamaindex";
import { VectorStoreIndex } from "llamaindex";
import { MongoDBAtlasVectorSearch } from "llamaindex/storage/vectorStore/MongoDBAtlasVectorStore";
import { MongoClient } from "mongodb";
import { checkRequiredEnvVars } from "./shared";
import { checkRequiredEnvVars, POPULATED_METADATA_FIELDS } from "./shared";
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const client = new MongoClient(process.env.MONGO_URI!);
const client = new MongoClient(process.env.MONGODB_URI!);
const store = new MongoDBAtlasVectorSearch({
mongodbClient: client,
dbName: process.env.MONGODB_DATABASE!,
collectionName: process.env.MONGODB_VECTORS!,
indexName: process.env.MONGODB_VECTOR_INDEX,
indexedMetadataFields: POPULATED_METADATA_FIELDS,
embeddingDefinition: {
dimensions: process.env.EMBEDDING_DIM
? parseInt(process.env.EMBEDDING_DIM)
: 1536,
},
});
return await VectorStoreIndex.fromVectorStore(store);
@@ -5,6 +5,8 @@ const REQUIRED_ENV_VARS = [
"MONGODB_VECTOR_INDEX",
];
export const POPULATED_METADATA_FIELDS = ["private", "doc_id"]; // for filtering in MongoDB VectorSearchIndex
export function checkRequiredEnvVars() {
const missingEnvVars = REQUIRED_ENV_VARS.filter((envVar) => {
return !process.env[envVar];
@@ -12,7 +12,8 @@ generate = "app.engine.generate:generate_datasource"
[tool.poetry.dependencies]
python = "^3.11"
llama-index-agent-openai = ">=0.3.0,<0.4.0"
llama-index = "^0.11.4"
llama-index = "0.11.9"
llama-index-core = "0.11.9"
fastapi = "^0.112.2"
python-dotenv = "^1.0.0"
uvicorn = { extras = ["standard"], version = "^0.23.2" }
@@ -15,12 +15,12 @@
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon --watch dist/index.js\""
},
"dependencies": {
"ai": "^3.0.21",
"ai": "3.3.38",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"duck-duck-scrape": "^2.2.5",
"express": "^4.18.2",
"llamaindex": "0.5.20",
"llamaindex": "0.6.2",
"pdf2json": "3.0.5",
"ajv": "^8.12.0",
"@e2b/code-interpreter": "^0.0.5",
@@ -1,185 +0,0 @@
import {
ALL_AVAILABLE_MISTRAL_MODELS,
Anthropic,
GEMINI_EMBEDDING_MODEL,
GEMINI_MODEL,
Gemini,
GeminiEmbedding,
Groq,
MistralAI,
MistralAIEmbedding,
MistralAIEmbeddingModelType,
OpenAI,
OpenAIEmbedding,
Settings,
} from "llamaindex";
import { HuggingFaceEmbedding } from "llamaindex/embeddings/HuggingFaceEmbedding";
import { OllamaEmbedding } from "llamaindex/embeddings/OllamaEmbedding";
import { ALL_AVAILABLE_ANTHROPIC_MODELS } from "llamaindex/llm/anthropic";
import { Ollama } from "llamaindex/llm/ollama";
const CHUNK_SIZE = 512;
const CHUNK_OVERLAP = 20;
export const initSettings = async () => {
// HINT: you can delete the initialization code for unused model providers
console.log(`Using '${process.env.MODEL_PROVIDER}' model provider`);
if (!process.env.MODEL || !process.env.EMBEDDING_MODEL) {
throw new Error("'MODEL' and 'EMBEDDING_MODEL' env variables must be set.");
}
switch (process.env.MODEL_PROVIDER) {
case "ollama":
initOllama();
break;
case "groq":
initGroq();
break;
case "anthropic":
initAnthropic();
break;
case "gemini":
initGemini();
break;
case "mistral":
initMistralAI();
break;
case "azure-openai":
initAzureOpenAI();
break;
default:
initOpenAI();
break;
}
Settings.chunkSize = CHUNK_SIZE;
Settings.chunkOverlap = CHUNK_OVERLAP;
};
function initOpenAI() {
Settings.llm = new OpenAI({
model: process.env.MODEL ?? "gpt-4o-mini",
maxTokens: process.env.LLM_MAX_TOKENS
? Number(process.env.LLM_MAX_TOKENS)
: undefined,
});
Settings.embedModel = new OpenAIEmbedding({
model: process.env.EMBEDDING_MODEL,
dimensions: process.env.EMBEDDING_DIM
? parseInt(process.env.EMBEDDING_DIM)
: undefined,
});
}
function initAzureOpenAI() {
// Map Azure OpenAI model names to OpenAI model names (only for TS)
const AZURE_OPENAI_MODEL_MAP: Record<string, string> = {
"gpt-35-turbo": "gpt-3.5-turbo",
"gpt-35-turbo-16k": "gpt-3.5-turbo-16k",
"gpt-4o": "gpt-4o",
"gpt-4": "gpt-4",
"gpt-4-32k": "gpt-4-32k",
"gpt-4-turbo": "gpt-4-turbo",
"gpt-4-turbo-2024-04-09": "gpt-4-turbo",
"gpt-4-vision-preview": "gpt-4-vision-preview",
"gpt-4-1106-preview": "gpt-4-1106-preview",
"gpt-4o-2024-05-13": "gpt-4o-2024-05-13",
};
const azureConfig = {
apiKey: process.env.AZURE_OPENAI_KEY,
endpoint: process.env.AZURE_OPENAI_ENDPOINT,
apiVersion:
process.env.AZURE_OPENAI_API_VERSION || process.env.OPENAI_API_VERSION,
};
Settings.llm = new OpenAI({
model:
AZURE_OPENAI_MODEL_MAP[process.env.MODEL ?? "gpt-35-turbo"] ??
"gpt-3.5-turbo",
maxTokens: process.env.LLM_MAX_TOKENS
? Number(process.env.LLM_MAX_TOKENS)
: undefined,
azure: {
...azureConfig,
deployment: process.env.AZURE_OPENAI_LLM_DEPLOYMENT,
},
});
Settings.embedModel = new OpenAIEmbedding({
model: process.env.EMBEDDING_MODEL,
dimensions: process.env.EMBEDDING_DIM
? parseInt(process.env.EMBEDDING_DIM)
: undefined,
azure: {
...azureConfig,
deployment: process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT,
},
});
}
function initOllama() {
const config = {
host: process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434",
};
Settings.llm = new Ollama({
model: process.env.MODEL ?? "",
config,
});
Settings.embedModel = new OllamaEmbedding({
model: process.env.EMBEDDING_MODEL ?? "",
config,
});
}
function initGroq() {
const embedModelMap: Record<string, string> = {
"all-MiniLM-L6-v2": "Xenova/all-MiniLM-L6-v2",
"all-mpnet-base-v2": "Xenova/all-mpnet-base-v2",
};
const modelMap: Record<string, string> = {
"llama3-8b": "llama3-8b-8192",
"llama3-70b": "llama3-70b-8192",
"mixtral-8x7b": "mixtral-8x7b-32768",
};
Settings.llm = new Groq({
model: modelMap[process.env.MODEL!],
});
Settings.embedModel = new HuggingFaceEmbedding({
modelType: embedModelMap[process.env.EMBEDDING_MODEL!],
});
}
function initAnthropic() {
const embedModelMap: Record<string, string> = {
"all-MiniLM-L6-v2": "Xenova/all-MiniLM-L6-v2",
"all-mpnet-base-v2": "Xenova/all-mpnet-base-v2",
};
Settings.llm = new Anthropic({
model: process.env.MODEL as keyof typeof ALL_AVAILABLE_ANTHROPIC_MODELS,
});
Settings.embedModel = new HuggingFaceEmbedding({
modelType: embedModelMap[process.env.EMBEDDING_MODEL!],
});
}
function initGemini() {
Settings.llm = new Gemini({
model: process.env.MODEL as GEMINI_MODEL,
});
Settings.embedModel = new GeminiEmbedding({
model: process.env.EMBEDDING_MODEL as GEMINI_EMBEDDING_MODEL,
});
}
function initMistralAI() {
Settings.llm = new MistralAI({
model: process.env.MODEL as keyof typeof ALL_AVAILABLE_MISTRAL_MODELS,
});
Settings.embedModel = new MistralAIEmbedding({
model: process.env.EMBEDDING_MODEL as MistralAIEmbeddingModelType,
});
}
@@ -29,6 +29,7 @@ export default function ChatSection() {
const message = JSON.parse(error.message);
alert(message.detail);
},
sendExtraMessageFields: true,
});
return (
@@ -68,7 +68,7 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
3,
true,
)}${fileExtension}`;
const fileName = window.prompt("Enter file name" || "", suggestedFileName);
const fileName = window.prompt("Enter file name", suggestedFileName);
if (!fileName) {
// User pressed cancel on prompt.
@@ -133,7 +133,11 @@ export default function Markdown({
return <></>;
}
}
return <a href={href}>{children}</a>;
return (
<a href={href} target="_blank">
{children}
</a>
);
},
}}
>
@@ -2,11 +2,15 @@
.custom-markdown ul {
list-style-type: disc;
margin-left: 20px;
margin-top: 20px;
margin-bottom: 20px;
}
.custom-markdown ol {
list-style-type: decimal;
margin-left: 20px;
margin-top: 20px;
margin-bottom: 20px;
}
.custom-markdown li {
@@ -21,3 +25,55 @@
.custom-markdown ol ol {
margin-left: 20px;
}
.custom-markdown img {
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin: 10px 0;
}
.custom-markdown a {
text-decoration: underline;
color: #007bff;
}
.custom-markdown h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: bold;
margin-bottom: 20px;
margin-top: 20px;
}
.custom-markdown h6 {
font-size: 16px;
}
.custom-markdown h5 {
font-size: 18px;
}
.custom-markdown h4 {
font-size: 20px;
}
.custom-markdown h3 {
font-size: 22px;
}
.custom-markdown h2 {
font-size: 24px;
}
.custom-markdown h1 {
font-size: 26px;
}
.custom-markdown hr {
border: 0;
border-top: 1px solid #e1e4e8;
margin: 20px 0;
}
@@ -17,7 +17,7 @@
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.0.2",
"ai": "^3.0.21",
"ai": "3.3.38",
"ajv": "^8.12.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -25,7 +25,7 @@
"duck-duck-scrape": "^2.2.5",
"formdata-node": "^6.0.3",
"got": "^14.4.1",
"llamaindex": "0.5.20",
"llamaindex": "0.6.2",
"lucide-react": "^0.294.0",
"next": "^14.2.4",
"react": "^18.2.0",