@falai/agent 0.3.0 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -24
- package/dist/cjs/core/Agent.d.ts +37 -0
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +167 -1
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/DomainRegistry.d.ts +10 -0
- package/dist/cjs/core/DomainRegistry.d.ts.map +1 -1
- package/dist/cjs/core/DomainRegistry.js +25 -0
- package/dist/cjs/core/DomainRegistry.js.map +1 -1
- package/dist/cjs/core/PromptBuilder.d.ts +9 -1
- package/dist/cjs/core/PromptBuilder.d.ts.map +1 -1
- package/dist/cjs/core/PromptBuilder.js +49 -2
- package/dist/cjs/core/PromptBuilder.js.map +1 -1
- package/dist/cjs/core/Route.d.ts +16 -0
- package/dist/cjs/core/Route.d.ts.map +1 -1
- package/dist/cjs/core/Route.js +22 -0
- package/dist/cjs/core/Route.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +43 -0
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -0
- package/dist/cjs/providers/AnthropicProvider.js +328 -0
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -0
- package/dist/cjs/providers/GeminiProvider.d.ts +4 -1
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +96 -0
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts +4 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +115 -0
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts +4 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +115 -0
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/providers/index.d.ts +13 -0
- package/dist/cjs/providers/index.d.ts.map +1 -0
- package/dist/cjs/providers/index.js +16 -0
- package/dist/cjs/providers/index.js.map +1 -0
- package/dist/cjs/types/ai.d.ts +28 -0
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/route.d.ts +6 -0
- package/dist/cjs/types/route.d.ts.map +1 -1
- package/dist/core/Agent.d.ts +37 -0
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +167 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/DomainRegistry.d.ts +10 -0
- package/dist/core/DomainRegistry.d.ts.map +1 -1
- package/dist/core/DomainRegistry.js +25 -0
- package/dist/core/DomainRegistry.js.map +1 -1
- package/dist/core/PromptBuilder.d.ts +9 -1
- package/dist/core/PromptBuilder.d.ts.map +1 -1
- package/dist/core/PromptBuilder.js +49 -2
- package/dist/core/PromptBuilder.js.map +1 -1
- package/dist/core/Route.d.ts +16 -0
- package/dist/core/Route.d.ts.map +1 -1
- package/dist/core/Route.js +22 -0
- package/dist/core/Route.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +43 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -0
- package/dist/providers/AnthropicProvider.js +321 -0
- package/dist/providers/AnthropicProvider.js.map +1 -0
- package/dist/providers/GeminiProvider.d.ts +4 -1
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +96 -0
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts +4 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +115 -0
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts +4 -1
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +115 -0
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +9 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/types/ai.d.ts +28 -0
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/route.d.ts +6 -0
- package/dist/types/route.d.ts.map +1 -1
- package/docs/API_REFERENCE.md +357 -2
- package/docs/CONSTRUCTOR_OPTIONS.md +178 -37
- package/docs/GETTING_STARTED.md +10 -2
- package/docs/PROVIDERS.md +139 -2
- package/examples/business-onboarding.ts +708 -0
- package/examples/declarative-agent.ts +1 -1
- package/examples/domain-scoping.ts +267 -0
- package/examples/healthcare-agent.ts +4 -4
- package/examples/openai-agent.ts +6 -4
- package/examples/rules-prohibitions.ts +258 -0
- package/examples/streaming-agent.ts +371 -0
- package/examples/travel-agent.ts +7 -4
- package/package.json +2 -1
- package/src/core/Agent.ts +220 -1
- package/src/core/DomainRegistry.ts +30 -0
- package/src/core/PromptBuilder.ts +70 -3
- package/src/core/Route.ts +28 -0
- package/src/index.ts +2 -0
- package/src/providers/AnthropicProvider.ts +467 -0
- package/src/providers/GeminiProvider.ts +135 -0
- package/src/providers/OpenAIProvider.ts +157 -0
- package/src/providers/OpenRouterProvider.ts +157 -0
- package/src/providers/index.ts +16 -0
- package/src/types/ai.ts +32 -0
- package/src/types/route.ts +6 -0
|
@@ -26,6 +26,7 @@ export enum BuiltInSection {
|
|
|
26
26
|
OBSERVATIONS = "observations",
|
|
27
27
|
CAPABILITIES = "capabilities",
|
|
28
28
|
ACTIVE_ROUTES = "active_routes",
|
|
29
|
+
DOMAINS = "domains",
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -431,7 +432,14 @@ When you detect any of these situations, consider which route would be most appr
|
|
|
431
432
|
* Add active routes information
|
|
432
433
|
*/
|
|
433
434
|
addActiveRoutes(
|
|
434
|
-
routes: Array<{
|
|
435
|
+
routes: Array<{
|
|
436
|
+
title: string;
|
|
437
|
+
description?: string;
|
|
438
|
+
conditions: string[];
|
|
439
|
+
domains?: string[];
|
|
440
|
+
rules?: string[];
|
|
441
|
+
prohibitions?: string[];
|
|
442
|
+
}>
|
|
435
443
|
): this {
|
|
436
444
|
if (routes.length > 0) {
|
|
437
445
|
const routesString = routes
|
|
@@ -441,7 +449,29 @@ When you detect any of these situations, consider which route would be most appr
|
|
|
441
449
|
? `\n Triggered when: ${route.conditions.join(" OR ")}`
|
|
442
450
|
: "";
|
|
443
451
|
const desc = route.description ? `\n ${route.description}` : "";
|
|
444
|
-
|
|
452
|
+
|
|
453
|
+
let domainInfo = "";
|
|
454
|
+
if (route.domains !== undefined) {
|
|
455
|
+
if (route.domains.length === 0) {
|
|
456
|
+
domainInfo = "\n Available tools: None (conversation only)";
|
|
457
|
+
} else {
|
|
458
|
+
domainInfo = `\n Available tools: ${route.domains.join(", ")}`;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let rulesInfo = "";
|
|
463
|
+
if (route.rules && route.rules.length > 0) {
|
|
464
|
+
const rulesList = route.rules.map((r, idx) => `${idx + 1}. ${r}`).join("; ");
|
|
465
|
+
rulesInfo = `\n RULES: ${rulesList}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let prohibitionsInfo = "";
|
|
469
|
+
if (route.prohibitions && route.prohibitions.length > 0) {
|
|
470
|
+
const prohibitionsList = route.prohibitions.map((p, idx) => `${idx + 1}. ${p}`).join("; ");
|
|
471
|
+
prohibitionsInfo = `\n PROHIBITIONS: ${prohibitionsList}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return `${i + 1}) ${route.title}${desc}${conditions}${domainInfo}${rulesInfo}${prohibitionsInfo}`;
|
|
445
475
|
})
|
|
446
476
|
.join("\n\n");
|
|
447
477
|
|
|
@@ -452,7 +482,11 @@ When you detect any of these situations, consider which route would be most appr
|
|
|
452
482
|
{routes_string}
|
|
453
483
|
###
|
|
454
484
|
|
|
455
|
-
These routes represent different paths the conversation can take. Choose the most appropriate route based on the user's needs
|
|
485
|
+
These routes represent different paths the conversation can take. Choose the most appropriate route based on the user's needs.
|
|
486
|
+
IMPORTANT:
|
|
487
|
+
- If a route specifies available tools, you can ONLY call tools from those domains when following that route.
|
|
488
|
+
- If a route has RULES, you MUST follow them when you choose that route.
|
|
489
|
+
- If a route has PROHIBITIONS, you MUST NEVER do those things when you choose that route.`,
|
|
456
490
|
{ routes_string: routesString },
|
|
457
491
|
SectionStatus.ACTIVE
|
|
458
492
|
);
|
|
@@ -460,6 +494,39 @@ These routes represent different paths the conversation can take. Choose the mos
|
|
|
460
494
|
return this;
|
|
461
495
|
}
|
|
462
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Add domains (tools) information
|
|
499
|
+
*/
|
|
500
|
+
addDomains(
|
|
501
|
+
domains: Record<string, Record<string, unknown>>
|
|
502
|
+
): this {
|
|
503
|
+
const domainNames = Object.keys(domains);
|
|
504
|
+
if (domainNames.length > 0) {
|
|
505
|
+
const domainsString = domainNames
|
|
506
|
+
.map((name, i) => {
|
|
507
|
+
const toolNames = Object.keys(domains[name]);
|
|
508
|
+
const tools = toolNames.join(", ");
|
|
509
|
+
return `${i + 1}) Domain "${name}": ${tools}`;
|
|
510
|
+
})
|
|
511
|
+
.join("\n");
|
|
512
|
+
|
|
513
|
+
this.addSection(
|
|
514
|
+
BuiltInSection.DOMAINS,
|
|
515
|
+
`Available tool domains:
|
|
516
|
+
###
|
|
517
|
+
{domains_string}
|
|
518
|
+
###
|
|
519
|
+
|
|
520
|
+
These are the tool domains registered in the system. Each domain contains specific tools/methods.
|
|
521
|
+
When calling tools, use the format: domain.toolName (e.g., "payment.processPayment").`,
|
|
522
|
+
{ domains_string: domainsString },
|
|
523
|
+
SectionStatus.ACTIVE
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return this;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
|
|
463
530
|
/**
|
|
464
531
|
* Add JSON response schema instructions
|
|
465
532
|
*/
|
package/src/core/Route.ts
CHANGED
|
@@ -16,6 +16,9 @@ export class Route<TContext = unknown> {
|
|
|
16
16
|
public readonly title: string;
|
|
17
17
|
public readonly description?: string;
|
|
18
18
|
public readonly conditions: string[];
|
|
19
|
+
public readonly domains?: string[];
|
|
20
|
+
public readonly rules: string[];
|
|
21
|
+
public readonly prohibitions: string[];
|
|
19
22
|
public readonly initialState: State<TContext>;
|
|
20
23
|
private guidelines: Guideline[] = [];
|
|
21
24
|
|
|
@@ -25,6 +28,9 @@ export class Route<TContext = unknown> {
|
|
|
25
28
|
this.title = options.title;
|
|
26
29
|
this.description = options.description;
|
|
27
30
|
this.conditions = options.conditions || [];
|
|
31
|
+
this.domains = options.domains;
|
|
32
|
+
this.rules = options.rules || ([] as string[]);
|
|
33
|
+
this.prohibitions = options.prohibitions || ([] as string[]);
|
|
28
34
|
this.initialState = new State<TContext>(this.id, "Initial state");
|
|
29
35
|
|
|
30
36
|
// Initialize guidelines from options
|
|
@@ -54,6 +60,28 @@ export class Route<TContext = unknown> {
|
|
|
54
60
|
return [...this.guidelines];
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Get allowed domain names for this route
|
|
65
|
+
* @returns Array of domain names, or undefined if all domains are allowed
|
|
66
|
+
*/
|
|
67
|
+
getDomains(): string[] | undefined {
|
|
68
|
+
return this.domains ? [...this.domains] : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get rules for this route
|
|
73
|
+
*/
|
|
74
|
+
getRules(): string[] {
|
|
75
|
+
return [...this.rules];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get prohibitions for this route
|
|
80
|
+
*/
|
|
81
|
+
getProhibitions(): string[] {
|
|
82
|
+
return [...this.prohibitions];
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
/**
|
|
58
86
|
* Get route reference
|
|
59
87
|
*/
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { OpenAIProvider } from "./providers/OpenAIProvider";
|
|
|
24
24
|
export type { OpenAIProviderOptions } from "./providers/OpenAIProvider";
|
|
25
25
|
export { OpenRouterProvider } from "./providers/OpenRouterProvider";
|
|
26
26
|
export type { OpenRouterProviderOptions } from "./providers/OpenRouterProvider";
|
|
27
|
+
export { AnthropicProvider } from "./providers/AnthropicProvider";
|
|
28
|
+
export type { AnthropicProviderOptions } from "./providers/AnthropicProvider";
|
|
27
29
|
|
|
28
30
|
// Constants
|
|
29
31
|
export { END_ROUTE } from "./constants";
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic (Claude) provider implementation with retry and backup models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
6
|
+
import type { MessageCreateParamsNonStreaming } from "@anthropic-ai/sdk/resources/messages";
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AiProvider,
|
|
10
|
+
GenerateMessageInput,
|
|
11
|
+
GenerateMessageOutput,
|
|
12
|
+
GenerateMessageStreamChunk,
|
|
13
|
+
AgentStructuredResponse,
|
|
14
|
+
} from "../types/ai";
|
|
15
|
+
import { withTimeoutAndRetry } from "../utils/retry";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
18
|
+
timeout: 60000,
|
|
19
|
+
retries: 3,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration options for Anthropic provider
|
|
24
|
+
* Uses types from @anthropic-ai/sdk package
|
|
25
|
+
*/
|
|
26
|
+
export interface AnthropicProviderOptions {
|
|
27
|
+
/** Anthropic API key */
|
|
28
|
+
apiKey: string;
|
|
29
|
+
/** Model to use (required) - e.g., "claude-sonnet-4-5", "claude-opus-4-1" */
|
|
30
|
+
model: string;
|
|
31
|
+
/** Backup models to try if primary fails (default: []) */
|
|
32
|
+
backupModels?: string[];
|
|
33
|
+
/** Default parameters - uses MessageCreateParamsNonStreaming from @anthropic-ai/sdk */
|
|
34
|
+
config?: Partial<Omit<MessageCreateParamsNonStreaming, "model" | "messages">>;
|
|
35
|
+
/** Retry configuration */
|
|
36
|
+
retryConfig?: {
|
|
37
|
+
timeout?: number;
|
|
38
|
+
retries?: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Type guard for errors with status/code properties
|
|
44
|
+
*/
|
|
45
|
+
interface ErrorWithStatus {
|
|
46
|
+
status?: number;
|
|
47
|
+
code?: string;
|
|
48
|
+
message?: string;
|
|
49
|
+
type?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Type guard to check if error is ErrorWithStatus
|
|
54
|
+
*/
|
|
55
|
+
function isErrorWithStatus(error: unknown): error is ErrorWithStatus {
|
|
56
|
+
return (
|
|
57
|
+
typeof error === "object" &&
|
|
58
|
+
error !== null &&
|
|
59
|
+
("status" in error || "code" in error || "message" in error)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Safely extract error message
|
|
65
|
+
*/
|
|
66
|
+
function getErrorMessage(error: unknown): string {
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
return error.message;
|
|
69
|
+
}
|
|
70
|
+
if (isErrorWithStatus(error) && error.message) {
|
|
71
|
+
return error.message;
|
|
72
|
+
}
|
|
73
|
+
return String(error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Determines if an error should trigger backup model usage
|
|
78
|
+
*/
|
|
79
|
+
const shouldUseBackupModel = (error: unknown): boolean => {
|
|
80
|
+
if (!isErrorWithStatus(error)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Server errors
|
|
85
|
+
if (error.status === 500 || error.status === 503 || error.status === 529) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Rate limiting
|
|
90
|
+
if (error.status === 429) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Model overloaded or unavailable
|
|
95
|
+
if (
|
|
96
|
+
error.type === "overloaded_error" ||
|
|
97
|
+
error.type === "api_error" ||
|
|
98
|
+
error.code === "overloaded"
|
|
99
|
+
) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const message = getErrorMessage(error);
|
|
104
|
+
if (
|
|
105
|
+
message.includes("overloaded") ||
|
|
106
|
+
message.includes("unavailable") ||
|
|
107
|
+
message.includes("internal error") ||
|
|
108
|
+
message.includes("Internal error")
|
|
109
|
+
) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Anthropic provider implementation with backup models and retry logic
|
|
118
|
+
*/
|
|
119
|
+
export class AnthropicProvider implements AiProvider {
|
|
120
|
+
public readonly name = "anthropic";
|
|
121
|
+
private client: Anthropic;
|
|
122
|
+
private primaryModel: string;
|
|
123
|
+
private backupModels: string[];
|
|
124
|
+
private config?: Partial<
|
|
125
|
+
Omit<MessageCreateParamsNonStreaming, "model" | "messages">
|
|
126
|
+
>;
|
|
127
|
+
private retryConfig: { timeout: number; retries: number };
|
|
128
|
+
|
|
129
|
+
constructor(options: AnthropicProviderOptions) {
|
|
130
|
+
const { apiKey, model, backupModels = [], config, retryConfig } = options;
|
|
131
|
+
|
|
132
|
+
if (!apiKey) {
|
|
133
|
+
throw new Error("Anthropic API key is required");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!model) {
|
|
137
|
+
throw new Error("Model is required. Example: 'claude-sonnet-4-5'");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.client = new Anthropic({
|
|
141
|
+
apiKey,
|
|
142
|
+
});
|
|
143
|
+
this.primaryModel = model;
|
|
144
|
+
this.backupModels = backupModels;
|
|
145
|
+
this.config = config;
|
|
146
|
+
this.retryConfig = {
|
|
147
|
+
timeout: retryConfig?.timeout || DEFAULT_RETRY_CONFIG.timeout,
|
|
148
|
+
retries: retryConfig?.retries || DEFAULT_RETRY_CONFIG.retries,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async generateMessage<TContext = unknown>(
|
|
153
|
+
input: GenerateMessageInput<TContext>
|
|
154
|
+
): Promise<GenerateMessageOutput> {
|
|
155
|
+
return this.generateWithBackup(input);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async *generateMessageStream<TContext = unknown>(
|
|
159
|
+
input: GenerateMessageInput<TContext>
|
|
160
|
+
): AsyncGenerator<GenerateMessageStreamChunk> {
|
|
161
|
+
yield* this.generateStreamWithBackup(input);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async generateWithBackup<TContext = unknown>(
|
|
165
|
+
input: GenerateMessageInput<TContext>
|
|
166
|
+
): Promise<GenerateMessageOutput> {
|
|
167
|
+
// Try primary model first
|
|
168
|
+
try {
|
|
169
|
+
return await this.generateWithModel(this.primaryModel, input);
|
|
170
|
+
} catch (primaryError: unknown) {
|
|
171
|
+
const primaryErrMsg = getErrorMessage(primaryError);
|
|
172
|
+
console.warn(
|
|
173
|
+
`[ANTHROPIC] Primary model ${this.primaryModel} failed: ${primaryErrMsg}`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!shouldUseBackupModel(primaryError)) {
|
|
177
|
+
throw primaryError;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`[ANTHROPIC] Trying backup models`);
|
|
181
|
+
|
|
182
|
+
let lastBackupError: unknown = primaryError;
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < this.backupModels.length; i++) {
|
|
185
|
+
const backupModel = this.backupModels[i];
|
|
186
|
+
console.log(
|
|
187
|
+
`[ANTHROPIC] Trying backup model ${i + 1}/${
|
|
188
|
+
this.backupModels.length
|
|
189
|
+
}: ${backupModel}`
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const result = await this.generateWithModel(backupModel, input);
|
|
194
|
+
console.log(`[ANTHROPIC] Backup model ${backupModel} succeeded`);
|
|
195
|
+
return result;
|
|
196
|
+
} catch (backupError: unknown) {
|
|
197
|
+
const backupErrMsg = getErrorMessage(backupError);
|
|
198
|
+
console.warn(
|
|
199
|
+
`[ANTHROPIC] Backup model ${backupModel} failed: ${backupErrMsg}`
|
|
200
|
+
);
|
|
201
|
+
lastBackupError = backupError;
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
!shouldUseBackupModel(backupError) &&
|
|
205
|
+
i < this.backupModels.length - 1
|
|
206
|
+
) {
|
|
207
|
+
console.log(
|
|
208
|
+
`[ANTHROPIC] Backup model error doesn't qualify for further attempts`
|
|
209
|
+
);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const lastBackupErrMsg = getErrorMessage(lastBackupError);
|
|
216
|
+
console.error(
|
|
217
|
+
`[ANTHROPIC] All models failed. Primary: ${primaryErrMsg}, Last backup: ${lastBackupErrMsg}`
|
|
218
|
+
);
|
|
219
|
+
throw lastBackupError;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async generateWithModel<TContext = unknown>(
|
|
224
|
+
model: string,
|
|
225
|
+
input: GenerateMessageInput<TContext>
|
|
226
|
+
): Promise<GenerateMessageOutput> {
|
|
227
|
+
const operation = async (): Promise<GenerateMessageOutput> => {
|
|
228
|
+
// Anthropic requires max_tokens to be specified
|
|
229
|
+
const maxTokens = input.parameters?.maxOutputTokens || 4096;
|
|
230
|
+
|
|
231
|
+
const params: MessageCreateParamsNonStreaming = {
|
|
232
|
+
model,
|
|
233
|
+
max_tokens: maxTokens,
|
|
234
|
+
messages: [
|
|
235
|
+
{
|
|
236
|
+
role: "user",
|
|
237
|
+
content: input.prompt,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
...this.config,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Handle JSON mode if requested
|
|
244
|
+
// Note: Anthropic doesn't have a native JSON mode like OpenAI,
|
|
245
|
+
// but we can add it to the system prompt
|
|
246
|
+
if (input.parameters?.jsonMode) {
|
|
247
|
+
// Add system message requesting JSON output
|
|
248
|
+
const systemPrompt =
|
|
249
|
+
"You must respond with valid JSON only. Do not include any text outside the JSON structure.";
|
|
250
|
+
|
|
251
|
+
// Merge with existing system if present
|
|
252
|
+
if (typeof this.config?.system === "string") {
|
|
253
|
+
params.system = `${this.config.system}\n\n${systemPrompt}`;
|
|
254
|
+
} else if (Array.isArray(this.config?.system)) {
|
|
255
|
+
params.system = [
|
|
256
|
+
...this.config.system,
|
|
257
|
+
{
|
|
258
|
+
type: "text" as const,
|
|
259
|
+
text: systemPrompt,
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
} else {
|
|
263
|
+
params.system = systemPrompt;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const response = await this.client.messages.create(params);
|
|
268
|
+
|
|
269
|
+
// Extract text from response
|
|
270
|
+
const textContent = response.content.find(
|
|
271
|
+
(block) => block.type === "text"
|
|
272
|
+
);
|
|
273
|
+
const message = textContent?.type === "text" ? textContent.text : "";
|
|
274
|
+
|
|
275
|
+
if (!message) {
|
|
276
|
+
throw new Error("No response from Anthropic");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Parse JSON response if JSON mode was enabled
|
|
280
|
+
let structured: AgentStructuredResponse | undefined;
|
|
281
|
+
if (input.parameters?.jsonMode) {
|
|
282
|
+
try {
|
|
283
|
+
structured = JSON.parse(message) as AgentStructuredResponse;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.warn("[ANTHROPIC] Failed to parse JSON response:", error);
|
|
286
|
+
// Fall back to treating the message as plain text
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
message,
|
|
292
|
+
metadata: {
|
|
293
|
+
model: response.model,
|
|
294
|
+
stopReason: response.stop_reason,
|
|
295
|
+
tokensUsed:
|
|
296
|
+
response.usage.input_tokens + response.usage.output_tokens,
|
|
297
|
+
promptTokens: response.usage.input_tokens,
|
|
298
|
+
completionTokens: response.usage.output_tokens,
|
|
299
|
+
},
|
|
300
|
+
structured,
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
return withTimeoutAndRetry(
|
|
305
|
+
operation,
|
|
306
|
+
this.retryConfig.timeout,
|
|
307
|
+
this.retryConfig.retries,
|
|
308
|
+
`Anthropic ${model}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private async *generateStreamWithBackup<TContext = unknown>(
|
|
313
|
+
input: GenerateMessageInput<TContext>
|
|
314
|
+
): AsyncGenerator<GenerateMessageStreamChunk> {
|
|
315
|
+
// Try primary model first
|
|
316
|
+
try {
|
|
317
|
+
yield* this.generateStreamWithModel(this.primaryModel, input);
|
|
318
|
+
} catch (primaryError: unknown) {
|
|
319
|
+
const primaryErrMsg = getErrorMessage(primaryError);
|
|
320
|
+
console.warn(
|
|
321
|
+
`[ANTHROPIC] Primary model ${this.primaryModel} failed: ${primaryErrMsg}`
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (!shouldUseBackupModel(primaryError)) {
|
|
325
|
+
throw primaryError;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(`[ANTHROPIC] Trying backup models for streaming`);
|
|
329
|
+
|
|
330
|
+
let lastBackupError: unknown = primaryError;
|
|
331
|
+
|
|
332
|
+
for (let i = 0; i < this.backupModels.length; i++) {
|
|
333
|
+
const backupModel = this.backupModels[i];
|
|
334
|
+
console.log(
|
|
335
|
+
`[ANTHROPIC] Trying backup model ${i + 1}/${
|
|
336
|
+
this.backupModels.length
|
|
337
|
+
}: ${backupModel}`
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
yield* this.generateStreamWithModel(backupModel, input);
|
|
342
|
+
console.log(`[ANTHROPIC] Backup model ${backupModel} succeeded`);
|
|
343
|
+
return;
|
|
344
|
+
} catch (backupError: unknown) {
|
|
345
|
+
const backupErrMsg = getErrorMessage(backupError);
|
|
346
|
+
console.warn(
|
|
347
|
+
`[ANTHROPIC] Backup model ${backupModel} failed: ${backupErrMsg}`
|
|
348
|
+
);
|
|
349
|
+
lastBackupError = backupError;
|
|
350
|
+
|
|
351
|
+
if (
|
|
352
|
+
!shouldUseBackupModel(backupError) &&
|
|
353
|
+
i < this.backupModels.length - 1
|
|
354
|
+
) {
|
|
355
|
+
console.log(
|
|
356
|
+
`[ANTHROPIC] Backup model error doesn't qualify for further attempts`
|
|
357
|
+
);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const lastBackupErrMsg = getErrorMessage(lastBackupError);
|
|
364
|
+
console.error(
|
|
365
|
+
`[ANTHROPIC] All models failed. Primary: ${primaryErrMsg}, Last backup: ${lastBackupErrMsg}`
|
|
366
|
+
);
|
|
367
|
+
throw lastBackupError;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async *generateStreamWithModel<TContext = unknown>(
|
|
372
|
+
model: string,
|
|
373
|
+
input: GenerateMessageInput<TContext>
|
|
374
|
+
): AsyncGenerator<GenerateMessageStreamChunk> {
|
|
375
|
+
// Anthropic requires max_tokens to be specified
|
|
376
|
+
const maxTokens = input.parameters?.maxOutputTokens || 4096;
|
|
377
|
+
|
|
378
|
+
const params = {
|
|
379
|
+
model,
|
|
380
|
+
max_tokens: maxTokens,
|
|
381
|
+
messages: [
|
|
382
|
+
{
|
|
383
|
+
role: "user" as const,
|
|
384
|
+
content: input.prompt,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
stream: true,
|
|
388
|
+
...this.config,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// Handle JSON mode if requested
|
|
392
|
+
if (input.parameters?.jsonMode) {
|
|
393
|
+
const systemPrompt =
|
|
394
|
+
"You must respond with valid JSON only. Do not include any text outside the JSON structure.";
|
|
395
|
+
|
|
396
|
+
if (typeof this.config?.system === "string") {
|
|
397
|
+
params.system = `${this.config.system}\n\n${systemPrompt}`;
|
|
398
|
+
} else if (Array.isArray(this.config?.system)) {
|
|
399
|
+
params.system = [
|
|
400
|
+
...this.config.system,
|
|
401
|
+
{
|
|
402
|
+
type: "text" as const,
|
|
403
|
+
text: systemPrompt,
|
|
404
|
+
},
|
|
405
|
+
];
|
|
406
|
+
} else {
|
|
407
|
+
params.system = systemPrompt;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const stream = this.client.messages.stream(params);
|
|
412
|
+
|
|
413
|
+
let accumulated = "";
|
|
414
|
+
let currentModel = model;
|
|
415
|
+
let stopReason: string | undefined;
|
|
416
|
+
let inputTokens = 0;
|
|
417
|
+
let outputTokens = 0;
|
|
418
|
+
|
|
419
|
+
for await (const chunk of stream) {
|
|
420
|
+
if (chunk.type === "message_start") {
|
|
421
|
+
currentModel = chunk.message.model;
|
|
422
|
+
inputTokens = chunk.message.usage.input_tokens;
|
|
423
|
+
} else if (chunk.type === "content_block_delta") {
|
|
424
|
+
if (chunk.delta.type === "text_delta") {
|
|
425
|
+
const delta = chunk.delta.text;
|
|
426
|
+
accumulated += delta;
|
|
427
|
+
yield {
|
|
428
|
+
delta,
|
|
429
|
+
accumulated,
|
|
430
|
+
done: false,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
} else if (chunk.type === "message_delta") {
|
|
434
|
+
stopReason = chunk.delta.stop_reason || undefined;
|
|
435
|
+
outputTokens = chunk.usage.output_tokens;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Parse JSON response if JSON mode was enabled
|
|
440
|
+
let structured: AgentStructuredResponse | undefined;
|
|
441
|
+
if (input.parameters?.jsonMode && accumulated) {
|
|
442
|
+
try {
|
|
443
|
+
structured = JSON.parse(accumulated) as AgentStructuredResponse;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.warn(
|
|
446
|
+
"[ANTHROPIC] Failed to parse JSON response in stream:",
|
|
447
|
+
error
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Yield final chunk
|
|
453
|
+
yield {
|
|
454
|
+
delta: "",
|
|
455
|
+
accumulated,
|
|
456
|
+
done: true,
|
|
457
|
+
metadata: {
|
|
458
|
+
model: currentModel,
|
|
459
|
+
stopReason,
|
|
460
|
+
tokensUsed: inputTokens + outputTokens,
|
|
461
|
+
promptTokens: inputTokens,
|
|
462
|
+
completionTokens: outputTokens,
|
|
463
|
+
},
|
|
464
|
+
structured,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|