fix(core): multple chat on anthropic agent (#799)

This commit is contained in:
Alex Yang
2024-05-03 16:18:46 -05:00
committed by GitHub
parent e74fe88342
commit be5df5b01b
6 changed files with 769 additions and 53 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"llamaindex": patch
"@llamaindex/core-test": patch
"@llamaindex/core-e2e": patch
---
fix: anthropic agent on multiple chat
+47 -3
View File
@@ -4,7 +4,7 @@ import { AnthropicAgent } from "llamaindex/agent/anthropic";
import { extractText } from "llamaindex/llm/utils";
import { ok, strictEqual } from "node:assert";
import { beforeEach, test } from "node:test";
import { sumNumbersTool } from "./fixtures/tools.js";
import { getWeatherTool, sumNumbersTool } from "./fixtures/tools.js";
import { mockLLMEvent } from "./utils.js";
let llm: LLM;
@@ -118,14 +118,58 @@ await test("anthropic agent", async (t) => {
});
await t.test("sum numbers", async () => {
const openaiAgent = new AnthropicAgent({
const anthropicAgent = new AnthropicAgent({
tools: [sumNumbersTool],
});
const { response } = await openaiAgent.chat({
const { response } = await anthropicAgent.chat({
message: "how much is 1 + 1?",
});
ok(extractText(response.message.content).includes("2"));
});
});
await test("anthropic agent with multiple chat", async (t) => {
await mockLLMEvent(t, "anthropic-agent-multiple-chat");
await t.test("chat", async () => {
const agent = new AnthropicAgent({
tools: [getWeatherTool],
});
{
const { response } = await agent.chat({
message: 'Hello? Response to me "Yes"',
});
consola.debug("response:", response.message.content);
ok(extractText(response.message.content).includes("Yes"));
}
{
const { response } = await agent.chat({
message: 'Hello? Response to me "No"',
});
consola.debug("response:", response.message.content);
ok(extractText(response.message.content).includes("No"));
}
{
const { response } = await agent.chat({
message: 'Hello? Response to me "Maybe"',
});
consola.debug("response:", response.message.content);
ok(extractText(response.message.content).includes("Maybe"));
}
{
const { response } = await agent.chat({
message: "What is the weather in San Francisco?",
});
consola.debug("response:", response.message.content);
ok(extractText(response.message.content).includes("72"));
}
{
const { response } = await agent.chat({
message: "What is the weather in Shanghai?",
});
consola.debug("response:", response.message.content);
ok(extractText(response.message.content).includes("72"));
}
});
});
@@ -0,0 +1,552 @@
{
"llmEventStart": [
{
"id": "PRESERVE_0",
"messages": [
{
"role": "user",
"content": "Hello? Response to me \"Yes\""
}
]
},
{
"id": "PRESERVE_1",
"messages": [
{
"role": "user",
"content": "Hello? Response to me \"Yes\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Yes\".\n</thinking>\n\nYes"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"No\""
}
]
},
{
"id": "PRESERVE_2",
"messages": [
{
"role": "user",
"content": "Hello? Response to me \"Yes\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Yes\".\n</thinking>\n\nYes"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"No\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"No\".\n</thinking>\n\nNo"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"Maybe\""
}
]
},
{
"id": "PRESERVE_3",
"messages": [
{
"role": "user",
"content": "Hello? Response to me \"Yes\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Yes\".\n</thinking>\n\nYes"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"No\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"No\".\n</thinking>\n\nNo"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"Maybe\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Maybe\".\n</thinking>\n\nMaybe"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "What is the weather in San Francisco?"
}
]
},
{
"id": "PRESERVE_4",
"messages": [
{
"role": "user",
"content": "Hello? Response to me \"Yes\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Yes\".\n</thinking>\n\nYes"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"No\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"No\".\n</thinking>\n\nNo"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"Maybe\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Maybe\".\n</thinking>\n\nMaybe"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "What is the weather in San Francisco?"
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has asked for the weather in a specific city, San Francisco. The relevant tool to answer this is the getWeather function.\n\nLooking at the required parameters for getWeather:\ncity (string): The user directly provided the city \"San Francisco\"\n\nSince the required \"city\" parameter has been provided, we can proceed with the getWeather function call.\n</thinking>"
}
],
"role": "assistant",
"options": {
"toolCall": {
"id": "toolu_01Gy7Gxbx7uGmjVncGH6pubL",
"name": "getWeather",
"input": {
"city": "San Francisco"
}
}
}
},
{
"content": "The weather in San Francisco is 72 degrees",
"role": "user",
"options": {
"toolResult": {
"result": "The weather in San Francisco is 72 degrees",
"isError": false,
"id": "toolu_01Gy7Gxbx7uGmjVncGH6pubL"
}
}
}
]
},
{
"id": "PRESERVE_5",
"messages": [
{
"role": "user",
"content": "Hello? Response to me \"Yes\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Yes\".\n</thinking>\n\nYes"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"No\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"No\".\n</thinking>\n\nNo"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"Maybe\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Maybe\".\n</thinking>\n\nMaybe"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "What is the weather in San Francisco?"
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has asked for the weather in a specific city, San Francisco. The relevant tool to answer this is the getWeather function.\n\nLooking at the required parameters for getWeather:\ncity (string): The user directly provided the city \"San Francisco\"\n\nSince the required \"city\" parameter has been provided, we can proceed with the getWeather function call.\n</thinking>"
}
],
"role": "assistant",
"options": {
"toolCall": {
"id": "toolu_01Gy7Gxbx7uGmjVncGH6pubL",
"name": "getWeather",
"input": {
"city": "San Francisco"
}
}
}
},
{
"content": "The weather in San Francisco is 72 degrees",
"role": "user",
"options": {
"toolResult": {
"result": "The weather in San Francisco is 72 degrees",
"isError": false,
"id": "toolu_01Gy7Gxbx7uGmjVncGH6pubL"
}
}
},
{
"content": [
{
"type": "text",
"text": "The current weather in San Francisco is 72 degrees."
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "What is the weather in Shanghai?"
}
]
},
{
"id": "PRESERVE_6",
"messages": [
{
"role": "user",
"content": "Hello? Response to me \"Yes\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Yes\".\n</thinking>\n\nYes"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"No\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"No\".\n</thinking>\n\nNo"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "Hello? Response to me \"Maybe\""
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Maybe\".\n</thinking>\n\nMaybe"
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "What is the weather in San Francisco?"
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has asked for the weather in a specific city, San Francisco. The relevant tool to answer this is the getWeather function.\n\nLooking at the required parameters for getWeather:\ncity (string): The user directly provided the city \"San Francisco\"\n\nSince the required \"city\" parameter has been provided, we can proceed with the getWeather function call.\n</thinking>"
}
],
"role": "assistant",
"options": {
"toolCall": {
"id": "toolu_01Gy7Gxbx7uGmjVncGH6pubL",
"name": "getWeather",
"input": {
"city": "San Francisco"
}
}
}
},
{
"content": "The weather in San Francisco is 72 degrees",
"role": "user",
"options": {
"toolResult": {
"result": "The weather in San Francisco is 72 degrees",
"isError": false,
"id": "toolu_01Gy7Gxbx7uGmjVncGH6pubL"
}
}
},
{
"content": [
{
"type": "text",
"text": "The current weather in San Francisco is 72 degrees."
}
],
"role": "assistant",
"options": {}
},
{
"role": "user",
"content": "What is the weather in Shanghai?"
},
{
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has asked for the weather in a specific city, Shanghai. The relevant tool to answer this is the getWeather function.\n\nLooking at the required parameters for getWeather:\ncity (string): The user directly provided the city \"Shanghai\"\n\nSince the required \"city\" parameter has been provided, we can proceed with the getWeather function call.\n</thinking>"
}
],
"role": "assistant",
"options": {
"toolCall": {
"id": "toolu_01NHyahSUqrPjxQk9mvCvvGe",
"name": "getWeather",
"input": {
"city": "Shanghai"
}
}
}
},
{
"content": "The weather in Shanghai is 72 degrees",
"role": "user",
"options": {
"toolResult": {
"result": "The weather in Shanghai is 72 degrees",
"isError": false,
"id": "toolu_01NHyahSUqrPjxQk9mvCvvGe"
}
}
}
]
}
],
"llmEventEnd": [
{
"id": "PRESERVE_0",
"response": {
"raw": null,
"message": {
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Yes\".\n</thinking>\n\nYes"
}
],
"role": "assistant",
"options": {}
}
}
},
{
"id": "PRESERVE_1",
"response": {
"raw": null,
"message": {
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"No\".\n</thinking>\n\nNo"
}
],
"role": "assistant",
"options": {}
}
}
},
{
"id": "PRESERVE_2",
"response": {
"raw": null,
"message": {
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has not asked a question that requires using any of the available tools. They have simply requested that I respond with the word \"Maybe\".\n</thinking>\n\nMaybe"
}
],
"role": "assistant",
"options": {}
}
}
},
{
"id": "PRESERVE_3",
"response": {
"raw": null,
"message": {
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has asked for the weather in a specific city, San Francisco. The relevant tool to answer this is the getWeather function.\n\nLooking at the required parameters for getWeather:\ncity (string): The user directly provided the city \"San Francisco\"\n\nSince the required \"city\" parameter has been provided, we can proceed with the getWeather function call.\n</thinking>"
}
],
"role": "assistant",
"options": {
"toolCall": {
"id": "toolu_01Gy7Gxbx7uGmjVncGH6pubL",
"name": "getWeather",
"input": {
"city": "San Francisco"
}
}
}
}
}
},
{
"id": "PRESERVE_4",
"response": {
"raw": null,
"message": {
"content": [
{
"type": "text",
"text": "The current weather in San Francisco is 72 degrees."
}
],
"role": "assistant",
"options": {}
}
}
},
{
"id": "PRESERVE_5",
"response": {
"raw": null,
"message": {
"content": [
{
"type": "text",
"text": "<thinking>\nThe user has asked for the weather in a specific city, Shanghai. The relevant tool to answer this is the getWeather function.\n\nLooking at the required parameters for getWeather:\ncity (string): The user directly provided the city \"Shanghai\"\n\nSince the required \"city\" parameter has been provided, we can proceed with the getWeather function call.\n</thinking>"
}
],
"role": "assistant",
"options": {
"toolCall": {
"id": "toolu_01NHyahSUqrPjxQk9mvCvvGe",
"name": "getWeather",
"input": {
"city": "Shanghai"
}
}
}
}
}
},
{
"id": "PRESERVE_6",
"response": {
"raw": null,
"message": {
"content": [
{
"type": "text",
"text": "The current weather in Shanghai is 72 degrees."
}
],
"role": "assistant",
"options": {}
}
}
}
],
"llmEventStream": []
}
+2 -1
View File
@@ -348,6 +348,8 @@ export abstract class AgentRunner<
>,
) => {
for await (const stepOutput of iter) {
// update chat history for each round
this.#chatHistory = [...stepOutput.taskStep.context.store.messages];
if (stepOutput.isLast) {
return stepOutput;
}
@@ -356,7 +358,6 @@ export abstract class AgentRunner<
},
);
const { output, taskStep } = stepOutput;
this.#chatHistory = [...taskStep.context.store.messages];
if (isAsyncIterable(output)) {
return output.pipeThrough<
AgentStreamChatResponse<AdditionalMessageOptions>
+95 -49
View File
@@ -156,58 +156,104 @@ export class Anthropic extends ToolCallLLM<AnthropicAdditionalChatOptions> {
formatMessages<Beta = false>(
messages: ChatMessage<ToolCallLLMMessageOptions>[],
): Beta extends true ? ToolsBetaMessageParam[] : MessageParam[] {
return messages.map<any>((message) => {
if (message.role !== "user" && message.role !== "assistant") {
throw new Error("Unsupported Anthropic role");
}
const options = message.options ?? {};
if ("toolResult" in options) {
const { id, isError } = options.toolResult;
return {
role: "user",
content: [
{
type: "tool_result",
is_error: isError,
content: [
{
type: "text",
text: extractText(message.content),
},
],
tool_use_id: id,
},
] satisfies ToolResultBlockParam[],
} satisfies ToolsBetaMessageParam;
} else if ("toolCall" in options) {
const aiThinkingText = extractText(message.content);
return {
role: "assistant",
content: [
// this could be empty when you call two tools in one query
...(aiThinkingText.trim()
? [
const result: ToolsBetaMessageParam[] = messages
.filter(
(message) => message.role === "user" || message.role === "assistant",
)
.map((message) => {
const options = message.options ?? {};
if ("toolResult" in options) {
const { id, isError } = options.toolResult;
return {
role: "user",
content: [
{
type: "tool_result",
is_error: isError,
content: [
{
type: "text",
text: aiThinkingText,
} satisfies TextBlockParam,
]
: []),
{
type: "tool_use",
id: options.toolCall.id,
name: options.toolCall.name,
input: options.toolCall.input,
} satisfies ToolUseBlockParam,
] satisfies ToolsBetaContentBlock[],
} satisfies ToolsBetaMessageParam;
}
text: extractText(message.content),
},
],
tool_use_id: id,
},
] satisfies ToolResultBlockParam[],
} satisfies ToolsBetaMessageParam;
} else if ("toolCall" in options) {
const aiThinkingText = extractText(message.content);
return {
role: "assistant",
content: [
// this could be empty when you call two tools in one query
...(aiThinkingText.trim()
? [
{
type: "text",
text: aiThinkingText,
} satisfies TextBlockParam,
]
: []),
{
type: "tool_use",
id: options.toolCall.id,
name: options.toolCall.name,
input: options.toolCall.input,
} satisfies ToolUseBlockParam,
] satisfies ToolsBetaContentBlock[],
} satisfies ToolsBetaMessageParam;
}
return {
content: extractText(message.content),
role: message.role,
} satisfies MessageParam;
});
return {
content: extractText(message.content),
role: message.role as "user" | "assistant",
} satisfies MessageParam;
});
// merge messages with the same role
// in case of 'messages: roles must alternate between "user" and "assistant", but found multiple "user" roles in a row'
const realResult: ToolsBetaMessageParam[] = [];
for (let i = 0; i < result.length; i++) {
if (i === 0) {
realResult.push(result[i]);
continue;
}
const current = result[i];
const previous = result[i - 1];
if (current.role === previous.role) {
// merge two messages with the same role
if (Array.isArray(previous.content)) {
if (Array.isArray(current.content)) {
previous.content.push(...current.content);
} else {
previous.content.push({
type: "text",
text: current.content,
});
}
} else {
if (Array.isArray(current.content)) {
previous.content = [
{
type: "text",
text: previous.content,
},
...current.content,
];
} else {
previous.content += `\n${current.content}`;
}
}
// no need to push the message
}
// if the roles are different, just push the message
else {
realResult.push(current);
}
}
return realResult as Beta extends true
? ToolsBetaMessageParam[]
: MessageParam[];
}
chat(
+66
View File
@@ -0,0 +1,66 @@
import { setEnvs } from "@llamaindex/env";
import { Anthropic } from "llamaindex";
import { beforeAll, describe, expect, test } from "vitest";
beforeAll(() => {
setEnvs({
ANTHROPIC_API_KEY: "valid",
});
});
describe("Anthropic llm", () => {
test("format messages", () => {
const anthropic = new Anthropic();
expect(
anthropic.formatMessages([
{
content: "You are a helpful assistant.",
role: "assistant",
},
{
content: "Hello?",
role: "user",
},
]),
).toEqual([
{
content: "You are a helpful assistant.",
role: "assistant",
},
{
content: "Hello?",
role: "user",
},
]);
expect(
anthropic.formatMessages([
{
content: "You are a helpful assistant.",
role: "assistant",
},
{
content: "Hello?",
role: "user",
},
{
content: "I am a system message.",
role: "system",
},
{
content: "What is your name?",
role: "user",
},
]),
).toEqual([
{
content: "You are a helpful assistant.",
role: "assistant",
},
{
content: "Hello?\nWhat is your name?",
role: "user",
},
]);
});
});