You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

211 lines
6.8 KiB

11 months ago
  1. const Anthropic = require("@anthropic-ai/sdk");
  2. const { RetryError } = require("../error.js");
  3. const Provider = require("./ai-provider.js");
  4. /**
  5. * The agent provider for the Anthropic API.
  6. * By default, the model is set to 'claude-2'.
  7. */
  8. class AnthropicProvider extends Provider {
  9. model;
  10. constructor(config = {}) {
  11. const {
  12. options = {
  13. apiKey: process.env.ANTHROPIC_API_KEY,
  14. maxRetries: 3,
  15. },
  16. model = "claude-2",
  17. } = config;
  18. const client = new Anthropic(options);
  19. super(client);
  20. this.model = model;
  21. }
  22. // For Anthropic we will always need to ensure the message sequence is role,content
  23. // as we can attach any data to message nodes and this keeps the message property
  24. // sent to the API always in spec.
  25. #sanitize(chats) {
  26. const sanitized = [...chats];
  27. // If the first message is not a USER, Anthropic will abort so keep shifting the
  28. // message array until that is the case.
  29. while (sanitized.length > 0 && sanitized[0].role !== "user")
  30. sanitized.shift();
  31. return sanitized.map((msg) => {
  32. const { role, content } = msg;
  33. return { role, content };
  34. });
  35. }
  36. #normalizeChats(messages = []) {
  37. if (!messages.length) return messages;
  38. const normalized = [];
  39. [...messages].forEach((msg, i) => {
  40. if (msg.role !== "function") return normalized.push(msg);
  41. // If the last message is a role "function" this is our special aibitat message node.
  42. // and we need to remove it from the array of messages.
  43. // Since Anthropic needs to have the tool call resolved, we look at the previous chat to "function"
  44. // and go through its content "thought" from ~ln:143 and get the tool_call id so we can resolve
  45. // this tool call properly.
  46. const functionCompletion = msg;
  47. const toolCallId = messages[i - 1]?.content?.find(
  48. (msg) => msg.type === "tool_use"
  49. )?.id;
  50. // Append the Anthropic acceptable node to the message chain so function can resolve.
  51. normalized.push({
  52. role: "user",
  53. content: [
  54. {
  55. type: "tool_result",
  56. tool_use_id: toolCallId,
  57. content: functionCompletion.content,
  58. },
  59. ],
  60. });
  61. });
  62. return normalized;
  63. }
  64. // Anthropic handles system message as a property, so here we split the system message prompt
  65. // from all the chats and then normalize them so they will be useable in case of tool_calls or general chat.
  66. #parseSystemPrompt(messages = []) {
  67. const chats = [];
  68. let systemPrompt =
  69. "You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions.";
  70. for (const msg of messages) {
  71. if (msg.role === "system") {
  72. systemPrompt = msg.content;
  73. continue;
  74. }
  75. chats.push(msg);
  76. }
  77. return [systemPrompt, this.#normalizeChats(chats)];
  78. }
  79. // Anthropic does not use the regular schema for functions so here we need to ensure it is in there specific format
  80. // so that the call can run correctly.
  81. #formatFunctions(functions = []) {
  82. return functions.map((func) => {
  83. const { name, description, parameters, required } = func;
  84. const { type, properties } = parameters;
  85. return {
  86. name,
  87. description,
  88. input_schema: {
  89. type,
  90. properties,
  91. required,
  92. },
  93. };
  94. });
  95. }
  96. /**
  97. * Create a completion based on the received messages.
  98. *
  99. * @param messages A list of messages to send to the Anthropic API.
  100. * @param functions
  101. * @returns The completion.
  102. */
  103. async complete(messages, functions = null) {
  104. try {
  105. const [systemPrompt, chats] = this.#parseSystemPrompt(messages);
  106. const response = await this.client.messages.create(
  107. {
  108. model: this.model,
  109. max_tokens: 4096,
  110. system: systemPrompt,
  111. messages: this.#sanitize(chats),
  112. stream: false,
  113. ...(Array.isArray(functions) && functions?.length > 0
  114. ? { tools: this.#formatFunctions(functions) }
  115. : {}),
  116. },
  117. { headers: { "anthropic-beta": "tools-2024-04-04" } } // Required to we can use tools.
  118. );
  119. // We know that we need to call a tool. So we are about to recurse through completions/handleExecution
  120. // https://docs.anthropic.com/claude/docs/tool-use#how-tool-use-works
  121. if (response.stop_reason === "tool_use") {
  122. // Get the tool call explicitly.
  123. const toolCall = response.content.find(
  124. (res) => res.type === "tool_use"
  125. );
  126. // Here we need the chain of thought the model may or may not have generated alongside the call.
  127. // this needs to be in a very specific format so we always ensure there is a 2-item content array
  128. // so that we can ensure the tool_call content is correct. For anthropic all text items must not
  129. // be empty, but the api will still return empty text so we need to make 100% sure text is not empty
  130. // or the tool call will fail.
  131. // wtf.
  132. let thought = response.content.find((res) => res.type === "text");
  133. thought =
  134. thought?.content?.length > 0
  135. ? {
  136. role: thought.role,
  137. content: [
  138. { type: "text", text: thought.content },
  139. { ...toolCall },
  140. ],
  141. }
  142. : {
  143. role: "assistant",
  144. content: [
  145. {
  146. type: "text",
  147. text: `Okay, im going to use ${toolCall.name} to help me.`,
  148. },
  149. { ...toolCall },
  150. ],
  151. };
  152. // Modify messages forcefully by adding system thought so that tool_use/tool_result
  153. // messaging works with Anthropic's disastrous tool calling API.
  154. messages.push(thought);
  155. const functionArgs = toolCall.input;
  156. return {
  157. result: null,
  158. functionCall: {
  159. name: toolCall.name,
  160. arguments: functionArgs,
  161. },
  162. cost: 0,
  163. };
  164. }
  165. const completion = response.content.find((msg) => msg.type === "text");
  166. return {
  167. result:
  168. completion?.text ??
  169. "The model failed to complete the task and return back a valid response.",
  170. cost: 0,
  171. };
  172. } catch (error) {
  173. // If invalid Auth error we need to abort because no amount of waiting
  174. // will make auth better.
  175. if (error instanceof Anthropic.AuthenticationError) throw error;
  176. if (
  177. error instanceof Anthropic.RateLimitError ||
  178. error instanceof Anthropic.InternalServerError ||
  179. error instanceof Anthropic.APIError // Also will catch AuthenticationError!!!
  180. ) {
  181. throw new RetryError(error.message);
  182. }
  183. throw error;
  184. }
  185. }
  186. }
  187. module.exports = AnthropicProvider;