feat(dependencies): update and add new packages for improved functionality

chore(config): remove sample.config.toml as it's no longer needed

refactor(webSearchAgent): enhance summary generation for better results

feat(providers): add RetryingChatAnthropic class for robust API calls

test(providers): implement test script for retry logic in Anthropic API

feat(testRetry.js): implement retry logic for Anthropic API calls

Add RetryingChatAnthropic class to handle API connection errors and
retry requests with exponential backoff. Improve reliability of API
calls in case of temporary network issues.
This commit is contained in:
XDA 2024-08-24 23:30:11 -04:00
parent cd28fca85d
commit be0da3ba76
8 changed files with 6603 additions and 770 deletions

5830
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/html-to-text": "^9.0.4",
"@types/node": "^22.4.1",
"@types/pdf-parse": "^1.1.4",
"@types/readable-stream": "^4.0.11",
"drizzle-kit": "^0.22.7",
@ -26,8 +27,9 @@
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@langchain/anthropic": "^0.2.3",
"@langchain/anthropic": "^0.2.15",
"@langchain/community": "^0.2.16",
"@langchain/core": "^0.2.27",
"@langchain/openai": "^0.0.25",
"@xenova/transformers": "^2.17.1",
"axios": "^1.6.8",

View file

@ -1,12 +0,0 @@
[GENERAL]
PORT = 3001 # Port to run the server on
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
[API_KEYS]
OPENAI = "" # OpenAI API key - sk-1234567890abcdef1234567890abcdef
GROQ = "" # Groq API key - gsk_1234567890abcdef1234567890abcdef
ANTHROPIC = "" # Anthropic API key - sk-ant-1234567890abcdef1234567890abcdef
[API_ENDPOINTS]
SEARXNG = "http://localhost:32768" # SearxNG API URL
OLLAMA = "" # Ollama API URL - http://host.docker.internal:11434

View file

@ -194,12 +194,13 @@ const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => {
await Promise.all(
docGroups.map(async (doc) => {
const res = await llm.invoke(`
You are a text summarizer. You need to summarize the text provided inside the \`text\` XML block.
You need to summarize the text into 1 or 2 sentences capturing the main idea of the text.
You need to make sure that you don't miss any point while summarizing the text.
You will also be given a \`query\` XML block which will contain the query of the user. Try to answer the query in the summary from the text provided.
If the query says Summarize then you just need to summarize the text without answering the query.
Only return the summarized text without any other messages, text or XML block.
You are a text summarizer. Summarize the text provided inside the \`text\` XML block into a single paragraph made up of five sentences.
Capture the main idea and include key supporting details to provide a more comprehensive summary.
Ensure you don't miss any significant points while summarizing.
If any specific legislation or regulations are mentioned, explicitly note them in your summary.
You will also be given a \`query\` XML block containing the user's query. Address this query in your summary if possible.
If the query says "Summarize", focus on providing a general summary without specifically answering a query.
Return only the summarized text without any additional messages, text, or XML blocks.
<query>
${question}
@ -209,7 +210,7 @@ const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => {
${doc.pageContent}
</text>
Make sure to answer the query in the summary.
Ensure your summary addresses the query (if applicable) and mentions any specific legislation or regulations.
`);
const document = new Document({
@ -377,4 +378,4 @@ const handleWebSearch = (
return emitter;
};
export default handleWebSearch;
export default handleWebSearch;

View file

@ -0,0 +1,76 @@
import { ChatAnthropic } from '@langchain/anthropic';
import { ChatMessage } from 'langchain/schema';
class RetryingChatAnthropic extends ChatAnthropic {
maxRetries: number;
baseDelay: number;
constructor(config: ConstructorParameters<typeof ChatAnthropic>[0] & { maxRetries?: number; baseDelay?: number }) {
super(config);
this.maxRetries = config.maxRetries || 3;
this.baseDelay = config.baseDelay || 1000;
}
async _call(messages: ChatMessage[], options: this['ParsedCallOptions'], runManager?: any): Promise<string> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await super._call(messages, options, runManager);
} catch (error) {
if (!(error instanceof Error && error.name === 'APIConnectionError') || attempt === this.maxRetries) {
throw error;
}
lastError = error;
const delay = this.calculateDelay(attempt);
console.warn(`APIConnectionError in ${this.modelName}: ${error.message}. Retrying in ${delay}ms. Attempt ${attempt + 1} of ${this.maxRetries + 1}`);
await this.wait(delay);
}
}
throw lastError;
}
private calculateDelay(attempt: number): number {
const jitter = Math.random() * 0.1 * this.baseDelay;
return Math.min(
(Math.pow(2, attempt) * this.baseDelay) + jitter,
30000
);
}
private wait(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
const loadAnthropicChatModel = async (): Promise<RetryingChatAnthropic> => {
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
if (!anthropicApiKey) {
throw new Error('Anthropic API key not found in environment variables');
}
return new RetryingChatAnthropic({
temperature: 0.7,
anthropicApiKey,
modelName: 'claude-3-sonnet-20240229',
maxRetries: 3,
baseDelay: 1000,
});
};
async function testRetryLogic(): Promise<void> {
try {
const claude = await loadAnthropicChatModel();
console.log("Model initialized successfully. Attempting API call...");
const result = await claude.call([
{ content: "Hello, Claude! Please provide a brief summary of your capabilities.", role: "human" }
]);
console.log("API call succeeded. Response:");
console.log(result);
} catch (error) {
console.error("Error occurred:", error);
}
}
testRetryLogic();

View file

@ -0,0 +1,131 @@
import { ChatAnthropic } from '@langchain/anthropic';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import axios from 'axios'; // We'll use axios to make a separate API call to identify our IP
class RetryingChatAnthropic extends ChatAnthropic {
maxRetries: number;
baseDelay: number;
constructor(config: ConstructorParameters<typeof ChatAnthropic>[0] & { maxRetries?: number; baseDelay?: number }) {
super(config);
this.maxRetries = config.maxRetries || 3;
this.baseDelay = config.baseDelay || 1000;
}
async call(messages: BaseMessage[], options?: this['ParsedCallOptions']): Promise<BaseMessage> {
let lastError: unknown;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
console.log(`Attempt ${attempt + 1}: Making API call to Anthropic...`);
const result = await super.call(messages, options);
console.log(`Attempt ${attempt + 1}: API call successful.`);
return result;
} catch (error) {
lastError = error;
console.error(`Full error object: ${JSON.stringify(error, Object.getOwnPropertyNames(error))}`);
if (this.isRetryableError(error) && attempt < this.maxRetries) {
const delay = this.calculateDelay(attempt);
console.warn(`Attempt ${attempt + 1}: Retryable error occurred. Retrying in ${delay}ms.`);
await this.wait(delay);
} else {
console.error(`Attempt ${attempt + 1}: Non-retryable error or max retries reached.`);
throw error;
}
}
}
throw lastError;
}
private isRetryableError(error: unknown): boolean {
if (error instanceof Error) {
// Check for network-related errors
if (error.message.includes('APIConnectionError') ||
error.message.includes('ETIMEDOUT') ||
error.message.includes('ENOTFOUND') ||
error.message.includes('EBADF') ||
error.message.includes('Connection error')) {
return true;
}
// Check for specific API errors that might be retryable
if (error.message.includes('rate limit') ||
error.message.includes('timeout') ||
error.message.includes('temporary failure')) {
return true;
}
}
// Check for Anthropic-specific error types
const anthropicError = error as any;
if (anthropicError.type === 'not_found_error' ||
anthropicError.type === 'service_unavailable' ||
anthropicError.type === 'timeout_error') {
return true;
}
return false;
}
private calculateDelay(attempt: number): number {
const jitter = Math.random() * 0.1 * this.baseDelay;
return Math.min((Math.pow(2, attempt) * this.baseDelay) + jitter, 30000);
}
private wait(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
const loadAnthropicChatModel = async (): Promise<RetryingChatAnthropic> => {
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
if (!anthropicApiKey) {
throw new Error('Anthropic API key not found in environment variables');
}
return new RetryingChatAnthropic({
temperature: 0.7,
anthropicApiKey,
modelName: 'claude-3-sonnet-20240229',
maxRetries: 3,
baseDelay: 1000,
});
};
async function getPublicIP(): Promise<string> {
try {
const response = await axios.get('https://api.ipify.org?format=json');
return response.data.ip;
} catch (error) {
console.error('Error fetching public IP:', error);
return 'Unknown';
}
}
async function testRetryLogic(): Promise<void> {
try {
const claude = await loadAnthropicChatModel();
console.log("Model initialized successfully. Attempting API call...");
const result = await claude.call([
new HumanMessage("Hello, Claude! Please provide a brief summary of your capabilities.")
]);
console.log("\nAPI call succeeded. Response:");
console.log(result.content);
} catch (error) {
console.error("\nError occurred:");
if (error instanceof Error) {
console.error("Error name:", error.name);
console.error("Error message:", error.message);
console.error("Error stack:", error.stack);
} else if (typeof error === 'object' && error !== null) {
console.error("Anthropic API Error:", JSON.stringify(error, null, 2));
} else {
console.error("Unexpected error:", error);
}
}
}
console.log('Starting retry logic test...');
testRetryLogic();

76
testRetry.js Normal file
View file

@ -0,0 +1,76 @@
import { ChatAnthropic } from '@langchain/anthropic';
import { ChatMessage } from 'langchain/schema';
class RetryingChatAnthropic extends ChatAnthropic {
maxRetries: number;
baseDelay: number;
constructor(config: ConstructorParameters<typeof ChatAnthropic>[0] & { maxRetries?: number; baseDelay?: number }) {
super(config);
this.maxRetries = config.maxRetries || 3;
this.baseDelay = config.baseDelay || 1000;
}
async _call(messages: ChatMessage[], options: this['ParsedCallOptions'], runManager?: any): Promise<string> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await super._call(messages, options, runManager);
} catch (error) {
if (!(error instanceof Error && error.name === 'APIConnectionError') || attempt === this.maxRetries) {
throw error;
}
lastError = error;
const delay = this.calculateDelay(attempt);
console.warn(`APIConnectionError in ${this.modelName}: ${error.message}. Retrying in ${delay}ms. Attempt ${attempt + 1} of ${this.maxRetries + 1}`);
await this.wait(delay);
}
}
throw lastError;
}
private calculateDelay(attempt: number): number {
const jitter = Math.random() * 0.1 * this.baseDelay;
return Math.min(
(Math.pow(2, attempt) * this.baseDelay) + jitter,
30000
);
}
private wait(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
const loadAnthropicChatModel = async (): Promise<RetryingChatAnthropic> => {
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
if (!anthropicApiKey) {
throw new Error('Anthropic API key not found in environment variables');
}
return new RetryingChatAnthropic({
temperature: 0.7,
anthropicApiKey,
modelName: 'claude-3-sonnet-20240229',
maxRetries: 3,
baseDelay: 1000,
});
};
async function testRetryLogic(): Promise<void> {
try {
const claude = await loadAnthropicChatModel();
console.log("Model initialized successfully. Attempting API call...");
const result = await claude.call([
{ content: "Hello, Claude! Please provide a brief summary of your capabilities.", role: "human" }
]);
console.log("API call succeeded. Response:");
console.log(result);
} catch (error) {
console.error("Error occurred:", error);
}
}
testRetryLogic();

1227
yarn.lock

File diff suppressed because it is too large Load diff