@loopstack/claude-module 0.21.0

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.
Files changed (88) hide show
  1. package/dist/claude.module.d.ts +3 -0
  2. package/dist/claude.module.d.ts.map +1 -0
  3. package/dist/claude.module.js +40 -0
  4. package/dist/claude.module.js.map +1 -0
  5. package/dist/documents/claude-message-document.d.ts +14 -0
  6. package/dist/documents/claude-message-document.d.ts.map +1 -0
  7. package/dist/documents/claude-message-document.js +41 -0
  8. package/dist/documents/claude-message-document.js.map +1 -0
  9. package/dist/documents/claude-message-document.yaml +5 -0
  10. package/dist/documents/index.d.ts +2 -0
  11. package/dist/documents/index.d.ts.map +1 -0
  12. package/dist/documents/index.js +18 -0
  13. package/dist/documents/index.js.map +1 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +20 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/schemas/claude-generate-tool-base.schema.d.ts +20 -0
  19. package/dist/schemas/claude-generate-tool-base.schema.d.ts.map +1 -0
  20. package/dist/schemas/claude-generate-tool-base.schema.js +24 -0
  21. package/dist/schemas/claude-generate-tool-base.schema.js.map +1 -0
  22. package/dist/services/claude-client.service.d.ts +8 -0
  23. package/dist/services/claude-client.service.d.ts.map +1 -0
  24. package/dist/services/claude-client.service.js +40 -0
  25. package/dist/services/claude-client.service.js.map +1 -0
  26. package/dist/services/claude-messages-helper.service.d.ts +10 -0
  27. package/dist/services/claude-messages-helper.service.d.ts.map +1 -0
  28. package/dist/services/claude-messages-helper.service.js +39 -0
  29. package/dist/services/claude-messages-helper.service.js.map +1 -0
  30. package/dist/services/claude-tools-helper.service.d.ts +6 -0
  31. package/dist/services/claude-tools-helper.service.d.ts.map +1 -0
  32. package/dist/services/claude-tools-helper.service.js +43 -0
  33. package/dist/services/claude-tools-helper.service.js.map +1 -0
  34. package/dist/services/index.d.ts +4 -0
  35. package/dist/services/index.d.ts.map +1 -0
  36. package/dist/services/index.js +20 -0
  37. package/dist/services/index.js.map +1 -0
  38. package/dist/tools/claude-delegate-tool-call.tool.d.ts +19 -0
  39. package/dist/tools/claude-delegate-tool-call.tool.d.ts.map +1 -0
  40. package/dist/tools/claude-delegate-tool-call.tool.js +102 -0
  41. package/dist/tools/claude-delegate-tool-call.tool.js.map +1 -0
  42. package/dist/tools/claude-generate-document.tool.d.ts +10 -0
  43. package/dist/tools/claude-generate-document.tool.d.ts.map +1 -0
  44. package/dist/tools/claude-generate-document.tool.js +64 -0
  45. package/dist/tools/claude-generate-document.tool.js.map +1 -0
  46. package/dist/tools/claude-generate-object.tool.d.ts +33 -0
  47. package/dist/tools/claude-generate-object.tool.d.ts.map +1 -0
  48. package/dist/tools/claude-generate-object.tool.js +131 -0
  49. package/dist/tools/claude-generate-object.tool.js.map +1 -0
  50. package/dist/tools/claude-generate-text.tool.d.ts +32 -0
  51. package/dist/tools/claude-generate-text.tool.d.ts.map +1 -0
  52. package/dist/tools/claude-generate-text.tool.js +112 -0
  53. package/dist/tools/claude-generate-text.tool.js.map +1 -0
  54. package/dist/tools/index.d.ts +5 -0
  55. package/dist/tools/index.d.ts.map +1 -0
  56. package/dist/tools/index.js +21 -0
  57. package/dist/tools/index.js.map +1 -0
  58. package/dist/types/claude.types.d.ts +19 -0
  59. package/dist/types/claude.types.d.ts.map +1 -0
  60. package/dist/types/claude.types.js +9 -0
  61. package/dist/types/claude.types.js.map +1 -0
  62. package/dist/types/index.d.ts +2 -0
  63. package/dist/types/index.d.ts.map +1 -0
  64. package/dist/types/index.js +18 -0
  65. package/dist/types/index.js.map +1 -0
  66. package/dist/utils/cache.utils.d.ts +11 -0
  67. package/dist/utils/cache.utils.d.ts.map +1 -0
  68. package/dist/utils/cache.utils.js +36 -0
  69. package/dist/utils/cache.utils.js.map +1 -0
  70. package/package.json +76 -0
  71. package/src/claude.module.ts +32 -0
  72. package/src/documents/claude-message-document.ts +26 -0
  73. package/src/documents/claude-message-document.yaml +5 -0
  74. package/src/documents/index.ts +1 -0
  75. package/src/index.ts +3 -0
  76. package/src/schemas/claude-generate-tool-base.schema.ts +23 -0
  77. package/src/services/claude-client.service.ts +34 -0
  78. package/src/services/claude-messages-helper.service.ts +41 -0
  79. package/src/services/claude-tools-helper.service.ts +38 -0
  80. package/src/services/index.ts +3 -0
  81. package/src/tools/claude-delegate-tool-call.tool.ts +125 -0
  82. package/src/tools/claude-generate-document.tool.ts +66 -0
  83. package/src/tools/claude-generate-object.tool.ts +151 -0
  84. package/src/tools/claude-generate-text.tool.ts +120 -0
  85. package/src/tools/index.ts +4 -0
  86. package/src/types/claude.types.ts +34 -0
  87. package/src/types/index.ts +1 -0
  88. package/src/utils/cache.utils.ts +59 -0
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@loopstack/claude-module",
3
+ "displayName": "Loopstack Claude Module",
4
+ "description": "A collection of tools for performing AI actions using the Anthropic Claude API directly via the official SDK",
5
+ "keywords": [
6
+ "ai",
7
+ "anthropic",
8
+ "claude",
9
+ "llm",
10
+ "loopstack",
11
+ "tool"
12
+ ],
13
+ "version": "0.21.0",
14
+ "license": "Apache-2.0",
15
+ "author": {
16
+ "name": "Jakob Klippel",
17
+ "url": "https://www.linkedin.com/in/jakob-klippel/"
18
+ },
19
+ "main": "dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "exports": {
22
+ ".": "./dist/index.js",
23
+ "./src/*": "./src/*"
24
+ },
25
+ "scripts": {
26
+ "build": "nest build",
27
+ "compile": "tsc --noEmit",
28
+ "format": "prettier --write .",
29
+ "lint": "eslint .",
30
+ "test": "jest --passWithNoTests",
31
+ "watch": "nest build --watch"
32
+ },
33
+ "dependencies": {
34
+ "@anthropic-ai/sdk": "^0.39.0",
35
+ "@loopstack/common": "^0.23.0",
36
+ "@loopstack/core-ui-module": "^0.20.6",
37
+ "@nestjs/common": "^11.1.14",
38
+ "@nestjs/config": "^4.0.3",
39
+ "@nestjs/core": "^11.1.14",
40
+ "zod": "^4.3.6"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "src"
45
+ ],
46
+ "jest": {
47
+ "moduleFileExtensions": [
48
+ "js",
49
+ "json",
50
+ "ts"
51
+ ],
52
+ "rootDir": "src",
53
+ "testRegex": ".*\\.spec\\.ts$",
54
+ "transform": {
55
+ "^.+\\.ts$": "ts-jest"
56
+ },
57
+ "collectCoverageFrom": [
58
+ "**/*.(t|j)s"
59
+ ],
60
+ "coverageDirectory": "../coverage",
61
+ "testEnvironment": "node",
62
+ "maxWorkers": 1
63
+ },
64
+ "loopstack": {
65
+ "modules": [
66
+ {
67
+ "path": "src/claude.module.ts",
68
+ "className": "ClaudeModule"
69
+ }
70
+ ],
71
+ "installModes": [
72
+ "add",
73
+ "install"
74
+ ]
75
+ }
76
+ }
@@ -0,0 +1,32 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { CoreUiModule } from '@loopstack/core-ui-module';
3
+ import { ClaudeMessageDocument } from './documents';
4
+ import { ClaudeClientService, ClaudeMessagesHelperService, ClaudeToolsHelperService } from './services';
5
+ import { ClaudeDelegateToolCall, ClaudeGenerateDocument, ClaudeGenerateObject, ClaudeGenerateText } from './tools';
6
+
7
+ @Module({
8
+ imports: [CoreUiModule],
9
+ providers: [
10
+ // services
11
+ ClaudeClientService,
12
+ ClaudeMessagesHelperService,
13
+ ClaudeToolsHelperService,
14
+
15
+ // tools
16
+ ClaudeGenerateText,
17
+ ClaudeGenerateObject,
18
+ ClaudeGenerateDocument,
19
+ ClaudeDelegateToolCall,
20
+
21
+ // documents
22
+ ClaudeMessageDocument,
23
+ ],
24
+ exports: [
25
+ ClaudeGenerateText,
26
+ ClaudeGenerateObject,
27
+ ClaudeGenerateDocument,
28
+ ClaudeDelegateToolCall,
29
+ ClaudeMessageDocument,
30
+ ],
31
+ })
32
+ export class ClaudeModule {}
@@ -0,0 +1,26 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { z } from 'zod';
3
+ import { Document, Input } from '@loopstack/common';
4
+
5
+ export const ClaudeMessageDocumentSchema = z.object({
6
+ role: z.enum(['user', 'assistant']),
7
+ content: z.union([z.string(), z.array(z.any())]),
8
+ toolResults: z.array(z.any()).optional(),
9
+ });
10
+
11
+ export type ClaudeMessageDocumentContentType = z.infer<typeof ClaudeMessageDocumentSchema>;
12
+
13
+ @Injectable()
14
+ @Document({
15
+ config: {
16
+ type: 'document',
17
+ description: 'Claude Message Document. Stores messages in Anthropic native format.',
18
+ },
19
+ configFile: __dirname + '/claude-message-document.yaml',
20
+ })
21
+ export class ClaudeMessageDocument {
22
+ @Input({
23
+ schema: ClaudeMessageDocumentSchema,
24
+ })
25
+ content: z.infer<typeof ClaudeMessageDocumentSchema>;
26
+ }
@@ -0,0 +1,5 @@
1
+ tags:
2
+ - message
3
+ ui:
4
+ form:
5
+ widget: claude-message
@@ -0,0 +1 @@
1
+ export * from './claude-message-document';
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './claude.module';
2
+ export * from './tools';
3
+ export * from './documents';
@@ -0,0 +1,23 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ClaudeGenerateToolBaseSchema = z.object({
4
+ claude: z
5
+ .object({
6
+ model: z.string().optional(),
7
+ envApiKey: z.string().optional(),
8
+ maxTokens: z.number().optional(),
9
+ cache: z.boolean().optional(),
10
+ })
11
+ .optional(),
12
+ messages: z
13
+ .array(
14
+ z.object({
15
+ role: z.enum(['user', 'assistant']),
16
+ content: z.union([z.string(), z.array(z.any())]),
17
+ }),
18
+ )
19
+ .optional(),
20
+ system: z.string().optional(),
21
+ prompt: z.string().optional(),
22
+ messagesSearchTag: z.string().optional(),
23
+ });
@@ -0,0 +1,34 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { ClaudeModelConfig } from '../types';
4
+
5
+ @Injectable()
6
+ export class ClaudeClientService {
7
+ private getApiKey(envApiKey?: string): string {
8
+ const envVar = envApiKey ?? 'ANTHROPIC_API_KEY';
9
+ const apiKey = process.env[envVar];
10
+
11
+ if (!apiKey) {
12
+ throw new Error(`No API key found! Please make sure to provide "${envVar}" in your .env file.`);
13
+ }
14
+
15
+ return apiKey;
16
+ }
17
+
18
+ getClient(config?: ClaudeModelConfig): Anthropic {
19
+ const apiKey = this.getApiKey(config?.envApiKey);
20
+ return new Anthropic({ apiKey });
21
+ }
22
+
23
+ getModel(config?: ClaudeModelConfig): string {
24
+ const model = config?.model ?? process.env['CLAUDE_MODEL'];
25
+
26
+ if (!model) {
27
+ throw new Error(
28
+ 'No model defined. Please provide CLAUDE_MODEL env var or set the claude.model parameter in the tool args.',
29
+ );
30
+ }
31
+
32
+ return model;
33
+ }
34
+ }
@@ -0,0 +1,41 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { DocumentEntity } from '@loopstack/common';
4
+
5
+ @Injectable()
6
+ export class ClaudeMessagesHelperService {
7
+ private searchMessages(documents: DocumentEntity[], tag: string): Anthropic.MessageParam[] {
8
+ return documents
9
+ .filter((document) => !document.isInvalidated && document.tags?.includes(tag))
10
+ .sort((a, b) => a.index - b.index)
11
+ .flatMap((document) => {
12
+ const content = document.content as {
13
+ role: 'user' | 'assistant';
14
+ content: string | Anthropic.ContentBlockParam[];
15
+ toolResults?: Anthropic.ToolResultBlockParam[];
16
+ };
17
+
18
+ // Combined document: expand into assistant message + user tool_result message
19
+ if (content.toolResults?.length) {
20
+ return [
21
+ { role: content.role, content: content.content } as Anthropic.MessageParam,
22
+ { role: 'user' as const, content: content.toolResults } as Anthropic.MessageParam,
23
+ ];
24
+ }
25
+
26
+ return [{ role: content.role, content: content.content } as Anthropic.MessageParam];
27
+ });
28
+ }
29
+
30
+ getMessages(
31
+ documents: DocumentEntity[],
32
+ args: { messages?: Anthropic.MessageParam[]; messagesSearchTag?: string },
33
+ ): Anthropic.MessageParam[] {
34
+ let messages: Anthropic.MessageParam[] | undefined = args.messages;
35
+ if (!messages?.length) {
36
+ messages = this.searchMessages(documents, args.messagesSearchTag ?? 'message');
37
+ }
38
+
39
+ return messages;
40
+ }
41
+ }
@@ -0,0 +1,38 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { toJSONSchema } from 'zod';
4
+ import { ToolInterface, WorkflowInterface, getBlockArgsSchema, getBlockConfig } from '@loopstack/common';
5
+ import { getBlockTool } from '@loopstack/common';
6
+ import { WorkflowType } from '@loopstack/contracts/dist/types';
7
+
8
+ @Injectable()
9
+ export class ClaudeToolsHelperService {
10
+ getTools(tools: string[], parent: WorkflowInterface): Anthropic.Tool[] | undefined {
11
+ const toolDefinitions: Anthropic.Tool[] = [];
12
+
13
+ for (const toolName of tools) {
14
+ const tool = getBlockTool<ToolInterface>(parent, toolName);
15
+ if (!tool) {
16
+ throw new Error(`Tool with name ${toolName} not available in Workflow context.`);
17
+ }
18
+
19
+ const inputSchema = getBlockArgsSchema(tool);
20
+ const config = getBlockConfig<WorkflowType>(tool);
21
+ if (!config) {
22
+ throw new Error(`Block ${tool.constructor.name} is missing @BlockConfig decorator`);
23
+ }
24
+
25
+ if (inputSchema) {
26
+ const jsonSchema = toJSONSchema(inputSchema);
27
+
28
+ toolDefinitions.push({
29
+ name: toolName,
30
+ description: config.description ?? '',
31
+ input_schema: jsonSchema as Anthropic.Tool['input_schema'],
32
+ });
33
+ }
34
+ }
35
+
36
+ return toolDefinitions.length ? toolDefinitions : undefined;
37
+ }
38
+ }
@@ -0,0 +1,3 @@
1
+ export * from './claude-client.service';
2
+ export * from './claude-messages-helper.service';
3
+ export * from './claude-tools-helper.service';
@@ -0,0 +1,125 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { Logger } from '@nestjs/common';
3
+ import { z } from 'zod';
4
+ import {
5
+ Input,
6
+ RunContext,
7
+ Tool,
8
+ ToolInterface,
9
+ ToolResult,
10
+ WorkflowInterface,
11
+ WorkflowMetadataInterface,
12
+ getBlockTool,
13
+ } from '@loopstack/common';
14
+
15
+ const ClaudeDelegateToolCallSchema = z.object({
16
+ message: z.object({
17
+ id: z.string().optional(),
18
+ content: z.array(z.any()),
19
+ }),
20
+ document: z.string().optional(),
21
+ skipResponseMessage: z.boolean().optional(),
22
+ });
23
+
24
+ type ClaudeDelegateToolCallArgs = z.infer<typeof ClaudeDelegateToolCallSchema>;
25
+
26
+ @Tool({
27
+ config: {
28
+ description: 'Delegate tool calls from a Claude response.',
29
+ },
30
+ })
31
+ export class ClaudeDelegateToolCall implements ToolInterface<ClaudeDelegateToolCallArgs> {
32
+ private readonly logger = new Logger(ClaudeDelegateToolCall.name);
33
+
34
+ @Input({
35
+ schema: ClaudeDelegateToolCallSchema,
36
+ })
37
+ args: ClaudeDelegateToolCallArgs;
38
+
39
+ async execute(
40
+ args: ClaudeDelegateToolCallArgs,
41
+ ctx: RunContext,
42
+ parent: WorkflowInterface,
43
+ runtime: WorkflowMetadataInterface,
44
+ ): Promise<ToolResult> {
45
+ const contentBlocks = args.message.content as Anthropic.ContentBlock[];
46
+ const toolResults: Anthropic.ToolResultBlockParam[] = [];
47
+
48
+ for (const block of contentBlocks) {
49
+ if (block.type !== 'tool_use') {
50
+ continue;
51
+ }
52
+
53
+ const tool = getBlockTool<ToolInterface>(parent, block.name);
54
+
55
+ if (!tool) {
56
+ throw new Error(`Tool ${block.name} not found.`);
57
+ }
58
+
59
+ try {
60
+ const result: ToolResult = await tool.execute(block.input as Record<string, unknown>, ctx, parent, runtime);
61
+
62
+ toolResults.push({
63
+ type: 'tool_result',
64
+ tool_use_id: block.id,
65
+ content: JSON.stringify(result.data, null, 2),
66
+ });
67
+ } catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : String(error);
69
+ this.logger.error(`Tool "${block.name}" failed: ${errorMessage}`);
70
+
71
+ toolResults.push({
72
+ type: 'tool_result',
73
+ tool_use_id: block.id,
74
+ content: errorMessage,
75
+ is_error: true,
76
+ });
77
+ }
78
+ }
79
+
80
+ const resultData = { toolResults };
81
+
82
+ if (args.document && !args.skipResponseMessage) {
83
+ const docResult = await this.createResponseMessage(args, toolResults, ctx, parent, runtime);
84
+
85
+ return {
86
+ data: resultData,
87
+ effects: docResult.effects,
88
+ };
89
+ }
90
+
91
+ return {
92
+ data: resultData,
93
+ };
94
+ }
95
+
96
+ private async createResponseMessage(
97
+ args: ClaudeDelegateToolCallArgs,
98
+ toolResults: Anthropic.ToolResultBlockParam[],
99
+ ctx: RunContext,
100
+ parent: WorkflowInterface,
101
+ runtime: WorkflowMetadataInterface,
102
+ ): Promise<ToolResult> {
103
+ const createDocumentTool = getBlockTool<ToolInterface>(parent, 'createDocument');
104
+ if (!createDocumentTool) {
105
+ throw new Error('createDocument tool not found in parent context.');
106
+ }
107
+
108
+ return createDocumentTool.execute(
109
+ {
110
+ id: args.message.id,
111
+ document: args.document,
112
+ update: {
113
+ content: {
114
+ role: 'assistant',
115
+ content: args.message.content,
116
+ toolResults,
117
+ },
118
+ },
119
+ },
120
+ ctx,
121
+ parent,
122
+ runtime,
123
+ );
124
+ }
125
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ InjectTool,
3
+ Input,
4
+ RunContext,
5
+ Tool,
6
+ ToolInterface,
7
+ ToolResult,
8
+ WorkflowInterface,
9
+ WorkflowMetadataInterface,
10
+ getBlockTool,
11
+ } from '@loopstack/common';
12
+ import { CreateDocument } from '@loopstack/core-ui-module';
13
+ import {
14
+ ClaudeGenerateObject,
15
+ ClaudeGenerateObjectArgsType,
16
+ ClaudeGenerateObjectSchema,
17
+ } from './claude-generate-object.tool';
18
+
19
+ @Tool({
20
+ config: {
21
+ description: 'Generates a structured object using the Anthropic Claude API and creates it as a document',
22
+ },
23
+ })
24
+ export class ClaudeGenerateDocument implements ToolInterface<ClaudeGenerateObjectArgsType> {
25
+ @InjectTool() private claudeGenerateObject!: ClaudeGenerateObject;
26
+ @InjectTool() private createDocument!: CreateDocument;
27
+
28
+ @Input({
29
+ schema: ClaudeGenerateObjectSchema,
30
+ })
31
+ args: ClaudeGenerateObjectArgsType;
32
+
33
+ private getRequiredTool(name: string): ToolInterface {
34
+ const tool = getBlockTool<ToolInterface>(this, name);
35
+ if (tool === undefined) {
36
+ throw new Error(`Tool "${name}" is not available`);
37
+ }
38
+ return tool;
39
+ }
40
+
41
+ async execute(
42
+ args: ClaudeGenerateObjectArgsType,
43
+ ctx: RunContext,
44
+ parent: WorkflowInterface,
45
+ metadata: WorkflowMetadataInterface,
46
+ ): Promise<ToolResult> {
47
+ const generateResult = await this.getRequiredTool('claudeGenerateObject').execute(args, ctx, parent, metadata);
48
+ const documentResult = await this.getRequiredTool('createDocument').execute(
49
+ {
50
+ id: args.response.id,
51
+ document: args.response.document,
52
+ update: {
53
+ content: generateResult.data as unknown,
54
+ },
55
+ },
56
+ ctx,
57
+ parent,
58
+ metadata,
59
+ );
60
+
61
+ return {
62
+ ...documentResult,
63
+ metadata: generateResult.metadata,
64
+ };
65
+ }
66
+ }
@@ -0,0 +1,151 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { Inject } from '@nestjs/common';
3
+ import { toJSONSchema, z } from 'zod';
4
+ import {
5
+ DocumentInterface,
6
+ Input,
7
+ RunContext,
8
+ Tool,
9
+ ToolInterface,
10
+ ToolResult,
11
+ WorkflowInterface,
12
+ WorkflowMetadataInterface,
13
+ getBlockArgsSchema,
14
+ getBlockDocument,
15
+ } from '@loopstack/common';
16
+ import { ClaudeGenerateToolBaseSchema } from '../schemas/claude-generate-tool-base.schema';
17
+ import { ClaudeClientService } from '../services';
18
+ import { ClaudeMessagesHelperService } from '../services';
19
+ import { applyCacheBreakpoints } from '../utils/cache.utils';
20
+
21
+ export const ClaudeGenerateObjectSchema = ClaudeGenerateToolBaseSchema.extend({
22
+ response: z.object({
23
+ id: z.string().optional(),
24
+ document: z.string(),
25
+ }),
26
+ }).strict();
27
+
28
+ export type ClaudeGenerateObjectArgsType = z.infer<typeof ClaudeGenerateObjectSchema>;
29
+
30
+ const STRUCTURED_OUTPUT_TOOL_NAME = 'structured_output';
31
+
32
+ @Tool({
33
+ config: {
34
+ description: 'Generates a structured object using the Anthropic Claude API',
35
+ },
36
+ })
37
+ export class ClaudeGenerateObject implements ToolInterface<ClaudeGenerateObjectArgsType> {
38
+ @Inject()
39
+ private readonly claudeClientService: ClaudeClientService;
40
+ @Inject()
41
+ private readonly claudeMessagesHelperService: ClaudeMessagesHelperService;
42
+
43
+ @Input({
44
+ schema: ClaudeGenerateObjectSchema,
45
+ })
46
+ args: ClaudeGenerateObjectArgsType;
47
+
48
+ async execute(
49
+ args: ClaudeGenerateObjectArgsType,
50
+ ctx: RunContext,
51
+ parent: WorkflowInterface,
52
+ metadata: WorkflowMetadataInterface,
53
+ ): Promise<ToolResult> {
54
+ const client = this.claudeClientService.getClient(args.claude);
55
+ const model = this.claudeClientService.getModel(args.claude);
56
+
57
+ const messages: Anthropic.MessageParam[] = [];
58
+
59
+ if (args.prompt) {
60
+ messages.push({ role: 'user', content: args.prompt });
61
+ } else {
62
+ const resolved = this.claudeMessagesHelperService.getMessages(metadata.documents, {
63
+ messages: args.messages as Anthropic.MessageParam[],
64
+ messagesSearchTag: args.messagesSearchTag,
65
+ });
66
+ messages.push(...resolved);
67
+ }
68
+
69
+ const document = getBlockDocument<DocumentInterface>(parent, args.response.document);
70
+ if (!document) {
71
+ throw new Error(`Document with name "${args.response.document}" not found in tool execution context.`);
72
+ }
73
+
74
+ const responseSchema = getBlockArgsSchema(document);
75
+ if (!responseSchema) {
76
+ throw new Error('Claude object generation source document must have a schema.');
77
+ }
78
+
79
+ const jsonSchema = toJSONSchema(responseSchema);
80
+
81
+ const response = await this.handleGenerateObject(client, {
82
+ model,
83
+ messages,
84
+ system: args.system,
85
+ maxTokens: args.claude?.maxTokens,
86
+ inputSchema: jsonSchema as Anthropic.Tool['input_schema'],
87
+ cache: args.claude?.cache,
88
+ });
89
+
90
+ return {
91
+ data: response.data,
92
+ metadata: {
93
+ usage: {
94
+ inputTokens: response.usage.input_tokens,
95
+ outputTokens: response.usage.output_tokens,
96
+ cacheCreationInputTokens: response.usage.cache_creation_input_tokens ?? 0,
97
+ cacheReadInputTokens: response.usage.cache_read_input_tokens ?? 0,
98
+ },
99
+ },
100
+ };
101
+ }
102
+
103
+ private async handleGenerateObject(
104
+ client: Anthropic,
105
+ options: {
106
+ model: string;
107
+ messages: Anthropic.MessageParam[];
108
+ system?: string;
109
+ maxTokens?: number;
110
+ inputSchema: Anthropic.Tool['input_schema'];
111
+ cache?: boolean;
112
+ },
113
+ ): Promise<{ data: unknown; usage: Anthropic.Usage }> {
114
+ const baseTool: Anthropic.Tool = {
115
+ name: STRUCTURED_OUTPUT_TOOL_NAME,
116
+ description: 'Return the structured output matching the schema.',
117
+ input_schema: options.inputSchema,
118
+ };
119
+
120
+ const { system, tools, messages } = options.cache
121
+ ? applyCacheBreakpoints({ system: options.system, tools: [baseTool], messages: options.messages })
122
+ : { system: options.system, tools: [baseTool], messages: options.messages };
123
+
124
+ const startTime = performance.now();
125
+ try {
126
+ const response = await client.messages.create({
127
+ model: options.model,
128
+ messages,
129
+ max_tokens: options.maxTokens ?? 4096,
130
+ ...(system ? { system } : {}),
131
+ tools: tools!,
132
+ tool_choice: { type: 'tool', name: STRUCTURED_OUTPUT_TOOL_NAME },
133
+ });
134
+
135
+ const toolUseBlock = response.content.find((block): block is Anthropic.ToolUseBlock => block.type === 'tool_use');
136
+
137
+ if (!toolUseBlock) {
138
+ throw new Error('Claude did not return a tool_use block for structured output.');
139
+ }
140
+
141
+ return {
142
+ data: toolUseBlock.input,
143
+ usage: response.usage,
144
+ };
145
+ } catch (error) {
146
+ const errorResponseTime = performance.now() - startTime;
147
+ console.error(`Request failed after ${errorResponseTime}ms:`, error);
148
+ throw error;
149
+ }
150
+ }
151
+ }