mirror of
https://github.com/netbirdio/explain.git
synced 2026-05-22 18:44:28 -07:00
Add configurable tools support to Anthropic provider
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
d10539bf96
commit
33bc9fb789
Generated
+1128
-4
File diff suppressed because it is too large
Load Diff
+13
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@netbirdio/explain",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.5",
|
||||
"description": "Full-stack AI assistant library with React frontend components and Node.js backend handler",
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "./dist/client/index.js",
|
||||
@@ -17,11 +17,18 @@
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"client": ["./dist/client/index.d.ts"],
|
||||
"server": ["./dist/server/index.d.ts"]
|
||||
"client": [
|
||||
"./dist/client/index.d.ts"
|
||||
],
|
||||
"server": [
|
||||
"./dist/server/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": ["dist", "src"],
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
@@ -31,6 +38,7 @@
|
||||
"react-dom": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"lucide-react": ">=0.300.0"
|
||||
},
|
||||
"repository": {
|
||||
@@ -42,4 +50,4 @@
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Message } from "../types";
|
||||
import { AnthropicProvider } from "./providers/anthropic";
|
||||
import { DifyProvider } from "./providers/dify";
|
||||
import { OpenAIProvider } from "./providers/openai";
|
||||
import type { LLMProvider, Middleware } from "./providers/types";
|
||||
import type { LLMProvider, Middleware, Tool } from "./providers/types";
|
||||
|
||||
export type AssistantConfig = {
|
||||
provider: "anthropic" | "openai" | "dify" | LLMProvider;
|
||||
@@ -11,6 +11,7 @@ export type AssistantConfig = {
|
||||
baseUrl?: string;
|
||||
systemPrompt?: string;
|
||||
middleware?: Middleware[];
|
||||
tools?: Tool[];
|
||||
};
|
||||
|
||||
type HandlerOptions = {
|
||||
@@ -132,7 +133,7 @@ export function createAssistant(config: AssistantConfig) {
|
||||
systemPrompt = result.systemPrompt;
|
||||
}
|
||||
|
||||
const reply = await provider.chat(messages, systemPrompt);
|
||||
const reply = await provider.chat(messages, systemPrompt, config.tools);
|
||||
return { reply };
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@ export type { AssistantConfig } from "./handler";
|
||||
export { AnthropicProvider } from "./providers/anthropic";
|
||||
export { DifyProvider } from "./providers/dify";
|
||||
export { OpenAIProvider } from "./providers/openai";
|
||||
export type { LLMProvider, ProviderConfig, Middleware } from "./providers/types";
|
||||
export type { LLMProvider, ProviderConfig, Middleware, Tool } from "./providers/types";
|
||||
@@ -1,5 +1,15 @@
|
||||
import type { Message } from "../../types";
|
||||
import type { LLMProvider, ProviderConfig } from "./types";
|
||||
import type { LLMProvider, ProviderConfig, Tool } from "./types";
|
||||
|
||||
type AnthropicMessage = {
|
||||
role: "user" | "assistant";
|
||||
content: string | AnthropicContentBlock[];
|
||||
};
|
||||
|
||||
type AnthropicContentBlock =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
|
||||
| { type: "tool_result"; tool_use_id: string; content: string };
|
||||
|
||||
export class AnthropicProvider implements LLMProvider {
|
||||
private apiKey: string;
|
||||
@@ -10,44 +20,104 @@ export class AnthropicProvider implements LLMProvider {
|
||||
this.model = config.model || "claude-sonnet-4-20250514";
|
||||
}
|
||||
|
||||
async chat(messages: Message[], systemPrompt?: string): Promise<string> {
|
||||
const anthropicMessages = messages.map((m) => ({
|
||||
async chat(messages: Message[], systemPrompt?: string, tools?: Tool[]): Promise<string> {
|
||||
const anthropicMessages: AnthropicMessage[] = messages.map((m) => ({
|
||||
role: m.role === "context" || m.role === "system" ? ("user" as const) : m.role,
|
||||
content: m.role === "context" ? `[Context]: ${m.content}` : m.content,
|
||||
}));
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
max_tokens: 4096,
|
||||
messages: anthropicMessages,
|
||||
};
|
||||
const toolDefs = tools?.map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
input_schema: t.input_schema,
|
||||
}));
|
||||
|
||||
if (systemPrompt) {
|
||||
body.system = systemPrompt;
|
||||
const toolsByName = new Map(tools?.map((t) => [t.name, t]));
|
||||
|
||||
let lastTextResponse = "";
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const body: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
max_tokens: 4096,
|
||||
messages: anthropicMessages,
|
||||
};
|
||||
|
||||
if (systemPrompt) {
|
||||
body.system = systemPrompt;
|
||||
}
|
||||
|
||||
if (toolDefs && toolDefs.length > 0) {
|
||||
body.tools = toolDefs;
|
||||
}
|
||||
|
||||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": this.apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Anthropic API error ${response.status}: ${text}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const textBlock = data.content?.find(
|
||||
(block: { type: string }) => block.type === "text",
|
||||
);
|
||||
if (textBlock) {
|
||||
lastTextResponse = textBlock.text;
|
||||
}
|
||||
|
||||
if (data.stop_reason !== "tool_use") {
|
||||
if (!lastTextResponse) {
|
||||
throw new Error("No text content in Anthropic response");
|
||||
}
|
||||
return lastTextResponse;
|
||||
}
|
||||
|
||||
// Handle tool calls
|
||||
const toolUseBlocks = data.content.filter(
|
||||
(block: { type: string }) => block.type === "tool_use",
|
||||
);
|
||||
|
||||
anthropicMessages.push({ role: "assistant", content: data.content });
|
||||
|
||||
const toolResults: AnthropicContentBlock[] = [];
|
||||
for (const toolUse of toolUseBlocks) {
|
||||
const tool = toolsByName.get(toolUse.name);
|
||||
if (!tool) {
|
||||
toolResults.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: toolUse.id,
|
||||
content: `Error: unknown tool "${toolUse.name}"`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const result = await tool.execute(toolUse.input);
|
||||
toolResults.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: toolUse.id,
|
||||
content: result,
|
||||
});
|
||||
} catch (err) {
|
||||
toolResults.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: toolUse.id,
|
||||
content: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
anthropicMessages.push({ role: "user", content: toolResults });
|
||||
}
|
||||
|
||||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": this.apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Anthropic API error ${response.status}: ${text}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const textBlock = data.content?.find(
|
||||
(block: { type: string }) => block.type === "text",
|
||||
);
|
||||
if (!textBlock) {
|
||||
throw new Error("No text content in Anthropic response");
|
||||
}
|
||||
return textBlock.text;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
import type { Message } from "../../types";
|
||||
|
||||
export type Tool = {
|
||||
name: string;
|
||||
description: string;
|
||||
input_schema: Record<string, unknown>;
|
||||
execute: (input: Record<string, unknown>) => Promise<string>;
|
||||
};
|
||||
|
||||
export interface LLMProvider {
|
||||
chat(messages: Message[], systemPrompt?: string): Promise<string>;
|
||||
chat(messages: Message[], systemPrompt?: string, tools?: Tool[]): Promise<string>;
|
||||
}
|
||||
|
||||
export type ProviderConfig = {
|
||||
|
||||
Reference in New Issue
Block a user