agentnet 0.0.2 → 0.0.3

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/src/llm/gemini.js CHANGED
@@ -1,160 +1,175 @@
1
1
  import { GoogleGenAI } from '@google/genai'
2
2
  import { logger } from '../utils/logger.js'
3
3
  import { LLMError } from '../errors/index.js'
4
+ import { BaseLLM } from './base.js'
4
5
 
5
- const type = 'gemini'
6
+ /**
7
+ * Gemini LLM implementation
8
+ */
9
+ class GeminiLLM extends BaseLLM {
10
+ constructor() {
11
+ super('gemini');
12
+ }
6
13
 
7
- const getClient = async function () {
14
+ /**
15
+ * Initializes and returns a Gemini client
16
+ * @returns {Promise<GoogleGenAI>} The initialized Gemini client
17
+ * @throws {LLMError} If initialization fails
18
+ */
19
+ async getClient() {
20
+ this.checkApiKey('GEMINI_API_KEY');
21
+
8
22
  try {
9
- if (!process.env.GEMINI_API_KEY) {
10
- throw new LLMError(
11
- 'GEMINI_API_KEY environment variable is not set',
12
- 'gemini'
13
- );
14
- }
15
-
16
- logger.debug('Initializing Gemini client');
17
- return new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
23
+ logger.debug('Initializing Gemini client');
24
+ return new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
18
25
  } catch (error) {
19
- logger.error('Failed to initialize Gemini client', { error });
20
- throw new LLMError(
21
- `Failed to initialize Gemini client: ${error.message}`,
22
- 'gemini',
23
- { originalError: error }
24
- );
26
+ logger.error('Failed to initialize Gemini client', { error });
27
+ throw new LLMError(
28
+ `Failed to initialize Gemini client: ${error.message}`,
29
+ this.type,
30
+ { originalError: error }
31
+ );
25
32
  }
26
- }
33
+ }
27
34
 
28
- const callModel = async function (llmClientConfig, context) {
29
- const client = context.client;
30
- const toolsAndHandoffsMap = context.toolsAndHandoffsMap;
31
- const conversation = context.conversation;
32
- const input = {};
33
-
34
- Object.assign(input, llmClientConfig);
35
- input['contents'] = conversation;
35
+ /**
36
+ * Calls the Gemini model with the provided configuration and context
37
+ * @param {Object} llmClientConfig - Configuration for the Gemini model
38
+ * @param {Object} context - Context containing client, tools map and conversation
39
+ * @returns {Promise<Object>} The model response
40
+ * @throws {LLMError} If the API call fails
41
+ */
42
+ async callModel(llmClientConfig, context) {
43
+ const { client, toolsAndHandoffsMap, conversation } = context;
44
+ const input = { ...llmClientConfig, contents: conversation };
36
45
 
46
+ // Configure tools if provided
37
47
  if (input.config !== undefined && input.tools !== undefined) {
38
- input.config.tools = toolsAndHandoffsMap.tools;
48
+ input.config.tools = toolsAndHandoffsMap.tools;
39
49
  } else if (toolsAndHandoffsMap.tools.length > 0) {
40
- if (input.config == undefined) {
41
- input.config = {};
42
- }
43
- input.config.tools = [{ functionDeclarations: toolsAndHandoffsMap.tools }];
50
+ input.config = input.config || {};
51
+ input.config.tools = [{ functionDeclarations: toolsAndHandoffsMap.tools }];
44
52
  }
45
53
 
46
54
  logger.debug('Calling Gemini model', {
47
- model: input.model,
48
- conversationLength: conversation.length,
49
- toolsCount: toolsAndHandoffsMap.tools.length
55
+ model: input.model,
56
+ conversationLength: conversation.length,
57
+ toolsCount: toolsAndHandoffsMap.tools.length
50
58
  });
51
59
 
52
60
  try {
53
- const res = await client.models.generateContent(input);
54
- logger.debug('Gemini response received', {
55
- responseType: res.response?.candidates ? 'candidates' : 'unknown',
56
- hasContent: !!res.response?.candidates?.[0]?.content
57
- });
58
- return res;
61
+ const res = await client.models.generateContent(input);
62
+ logger.debug('Gemini response received', {
63
+ responseType: res.response?.candidates ? 'candidates' : 'unknown',
64
+ hasContent: !!res.response?.candidates?.[0]?.content
65
+ });
66
+ return res;
59
67
  } catch (error) {
60
- logger.error('Gemini API error', {
61
- error,
62
- modelName: input.model
63
- });
64
-
65
- throw new LLMError(
66
- `Gemini API error: ${error.message}`,
67
- 'gemini',
68
- {
69
- statusCode: error.status || error.statusCode,
70
- modelName: input.model
71
- }
72
- );
68
+ logger.error('Gemini API error', {
69
+ error,
70
+ modelName: input.model
71
+ });
72
+
73
+ throw new LLMError(
74
+ `Gemini API error: ${error.message}`,
75
+ this.type,
76
+ {
77
+ statusCode: error.status || error.statusCode,
78
+ modelName: input.model
79
+ }
80
+ );
73
81
  }
74
- }
82
+ }
75
83
 
76
- const onResponse = async function (state, conversation, toolsAndHandoffsMap, response) {
84
+ /**
85
+ * Handle a specific tool call from Gemini response
86
+ * @param {Object} toolCall - The tool call to process
87
+ * @param {Object} state - Current application state
88
+ * @param {Array} conversation - The conversation history
89
+ * @param {Object} toolsAndHandoffsMap - Map of available tools
90
+ */
91
+ async handleToolCall(toolCall, state, conversation, toolsAndHandoffsMap) {
92
+ const args = toolCall.args;
93
+ const name = toolCall.name;
94
+
95
+ try {
96
+ const result = await super.executeToolCall(toolCall, name, args, state, conversation, toolsAndHandoffsMap);
97
+
98
+ // Add function call and response to conversation in Gemini-specific format
99
+ const function_response_part = {
100
+ name: name,
101
+ response: typeof result === 'string' ? { answer: result } : result
102
+ };
103
+
104
+ conversation.push({ role: 'model', parts: [{ functionCall: toolCall }] });
105
+ conversation.push({ role: 'user', parts: [{ functionResponse: function_response_part }] });
106
+
107
+ } catch (error) {
108
+ // Return error as function response in Gemini-specific format
109
+ const errorResponse = {
110
+ name: name,
111
+ response: { error: error.message }
112
+ };
113
+
114
+ conversation.push({ role: 'model', parts: [{ functionCall: toolCall }] });
115
+ conversation.push({ role: 'user', parts: [{ functionResponse: errorResponse }] });
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Processes the model response, handling text responses and function calls
121
+ * @param {Object} state - Current application state
122
+ * @param {Array} conversation - The conversation history
123
+ * @param {Object} toolsAndHandoffsMap - Map of available tools
124
+ * @param {Object} response - The model response to process
125
+ * @returns {Promise<string|null>} Text response or null if processing tool calls
126
+ */
127
+ async onResponse(state, conversation, toolsAndHandoffsMap, response) {
128
+ // Handle simple text response
77
129
  if (response.text !== undefined) {
78
- logger.debug('Gemini response contains text, returning directly');
79
- conversation.push({ role: 'model', parts: [{ text: response.text }] });
80
- return response.text;
130
+ logger.debug('Gemini response contains text, returning directly');
131
+ conversation.push({ role: 'model', parts: [{ text: response.text }] });
132
+ return response.text;
81
133
  }
82
134
 
135
+ // Handle function calls
83
136
  logger.debug('Gemini response contains function calls', {
84
- functionCallCount: response.functionCalls?.length || 0
137
+ functionCallCount: response.functionCalls?.length || 0
85
138
  });
86
139
 
87
- for (const toolCall of response.functionCalls) {
88
- const args = toolCall.args;
89
- const name = toolCall.name;
90
-
91
- logger.debug('Executing tool from Gemini', {
92
- toolName: name,
93
- argsPreview: JSON.stringify(args).substring(0, 100)
94
- });
95
-
96
- try {
97
- if (!toolsAndHandoffsMap[name] || !toolsAndHandoffsMap[name].function) {
98
- throw new Error(`Tool "${name}" not found or has no function implementation`);
99
- }
100
-
101
- let result = null
102
- if (toolsAndHandoffsMap[name].type === 'handoff') {
103
- result = await toolsAndHandoffsMap[name].function(conversation, state, args);
104
- } else {
105
- result = await toolsAndHandoffsMap[name].function(state, args);
106
- }
107
- if (toolsAndHandoffsMap[name].type === 'handoff') {
108
- const resultParsed = JSON.parse(result)
109
- // Update state with the result
110
- if (resultParsed.session) {
111
- for (const key of Object.keys(resultParsed.session)) {
112
- state[key] = resultParsed.session[key]
113
- }
114
- }
115
- }
116
-
117
- const function_response_part = {
118
- name: name,
119
- response: typeof result === 'string' ? { answer: result } : result
120
- };
121
-
122
- // Append function call and result of the function execution to contents
123
- conversation.push({ role: 'model', parts: [{ functionCall: toolCall }] });
124
- conversation.push({ role: 'user', parts: [{ functionResponse: function_response_part }] });
125
-
126
- logger.debug('Tool execution successful', { toolName: name });
127
- } catch (error) {
128
- logger.error(`Error executing tool "${name}"`, { error });
129
- // Return error as function response
130
- const errorResponse = {
131
- name: name,
132
- response: { error: error.message }
133
- };
134
-
135
- conversation.push({ role: 'model', parts: [{ functionCall: toolCall }] });
136
- conversation.push({ role: 'user', parts: [{ functionResponse: errorResponse }] });
137
- }
140
+ // Process all tool calls sequentially
141
+ if (response.functionCalls?.length) {
142
+ for (const toolCall of response.functionCalls) {
143
+ await this.handleToolCall(toolCall, state, conversation, toolsAndHandoffsMap);
144
+ }
138
145
  }
139
146
 
140
147
  return null;
141
- }
148
+ }
142
149
 
143
- const prompt = async function (conversation, formattedPrompt) {
144
- logger.debug('Adding user prompt to conversation', {
145
- promptPreview: formattedPrompt.substring(0, 100)
146
- });
150
+ /**
151
+ * Adds a user prompt to the conversation
152
+ * @param {Array} conversation - The conversation history
153
+ * @param {string} formattedPrompt - The formatted user prompt
154
+ * @returns {Promise<void>}
155
+ */
156
+ async prompt(conversation, formattedPrompt) {
157
+ await super.prompt(conversation, formattedPrompt);
147
158
 
148
159
  conversation.push({
149
- role: 'user',
150
- parts: [{ text: formattedPrompt}]
160
+ role: 'user',
161
+ parts: [{ text: formattedPrompt }]
151
162
  });
163
+ }
152
164
  }
153
165
 
166
+ // Create a singleton instance
167
+ const geminiLLM = new GeminiLLM();
168
+
154
169
  export default {
155
- type,
156
- getClient,
157
- prompt,
158
- callModel,
159
- onResponse
170
+ type: geminiLLM.type,
171
+ getClient: geminiLLM.getClient.bind(geminiLLM),
172
+ prompt: geminiLLM.prompt.bind(geminiLLM),
173
+ callModel: geminiLLM.callModel.bind(geminiLLM),
174
+ onResponse: geminiLLM.onResponse.bind(geminiLLM)
160
175
  }
package/src/llm/gpt.js CHANGED
@@ -1,89 +1,88 @@
1
1
  import OpenAI from 'openai'
2
2
  import { logger } from '../utils/logger.js'
3
3
  import { LLMError } from '../errors/index.js'
4
+ import { BaseLLM } from './base.js'
4
5
 
5
- const type = 'openai'
6
+ /**
7
+ * OpenAI LLM implementation
8
+ */
9
+ class OpenAILLM extends BaseLLM {
10
+ constructor() {
11
+ super('openai');
12
+ }
6
13
 
7
- const getClient = async function () {
8
- try {
9
- if (!process.env.OPENAI_API_KEY) {
14
+ /**
15
+ * Initializes and returns an OpenAI client
16
+ * @returns {Promise<OpenAI>} The initialized OpenAI client
17
+ * @throws {LLMError} If initialization fails
18
+ */
19
+ async getClient() {
20
+ this.checkApiKey('OPENAI_API_KEY');
21
+
22
+ try {
23
+ logger.debug('Initializing OpenAI client');
24
+ return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
25
+ } catch (error) {
26
+ logger.error('Failed to initialize OpenAI client', { error });
10
27
  throw new LLMError(
11
- 'OPENAI_API_KEY environment variable is not set',
12
- 'openai'
28
+ `Failed to initialize OpenAI client: ${error.message}`,
29
+ this.type,
30
+ { originalError: error }
13
31
  );
14
32
  }
15
-
16
- logger.debug('Initializing OpenAI client');
17
- return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
18
- } catch (error) {
19
- logger.error('Failed to initialize OpenAI client', { error });
20
- throw new LLMError(
21
- `Failed to initialize OpenAI client: ${error.message}`,
22
- 'openai',
23
- { originalError: error }
24
- );
25
33
  }
26
- }
27
34
 
28
- const callModel = async function (llmClientConfig, context) {
29
- const client = context.client
30
- const toolsAndHandoffsMap = context.toolsAndHandoffsMap
31
- const conversation = context.conversation
32
- const input = {}
33
- Object.assign(input, llmClientConfig)
34
- input['tools'] = toolsAndHandoffsMap.tools
35
- input['input'] = conversation
36
-
37
- logger.debug('Calling OpenAI model', {
38
- model: input.model,
39
- conversationLength: conversation.length,
40
- toolsCount: toolsAndHandoffsMap.tools.length
41
- });
42
-
43
- try {
44
- const response = await client.responses.create(input)
45
- logger.debug('OpenAI response received');
46
- return response
47
- } catch (error) {
48
- logger.error('OpenAI API error', {
49
- error,
50
- modelName: input.model
35
+ /**
36
+ * Calls the OpenAI model with the provided configuration and context
37
+ * @param {Object} llmClientConfig - Configuration for the OpenAI model
38
+ * @param {Object} context - Context containing client, tools map and conversation
39
+ * @returns {Promise<Object>} The model response
40
+ * @throws {LLMError} If the API call fails
41
+ */
42
+ async callModel(llmClientConfig, context) {
43
+ const { client, toolsAndHandoffsMap, conversation } = context;
44
+ const input = { ...llmClientConfig };
45
+ input.tools = toolsAndHandoffsMap.tools;
46
+ input.input = conversation;
47
+
48
+ logger.debug('Calling OpenAI model', {
49
+ model: input.model,
50
+ conversationLength: conversation.length,
51
+ toolsCount: toolsAndHandoffsMap.tools.length
51
52
  });
52
53
 
53
- throw new LLMError(
54
- `OpenAI API error: ${error.message}`,
55
- 'openai',
56
- {
57
- statusCode: error.status || error.statusCode,
54
+ try {
55
+ const response = await client.responses.create(input);
56
+ logger.debug('OpenAI response received');
57
+ return response;
58
+ } catch (error) {
59
+ logger.error('OpenAI API error', {
60
+ error,
58
61
  modelName: input.model
59
- }
60
- );
61
- }
62
- }
63
-
64
- const onResponse = async function (state, conversation, toolsAndHandoffsMap, response) {
65
- if (response.output_text !== undefined && response.output_text.length > 0) {
66
- logger.debug('OpenAI response contains text, returning directly');
67
- conversation.push({ role: 'model', parts: [{ text: response.output_text }] });
68
- return response.output_text
69
- }
70
-
71
- const reasoning = response.output.filter(x => x.type == 'reasoning')
72
- const functionCalls = response.output.filter(x => x.type == 'function_call')
73
-
74
- logger.debug('OpenAI response processing', {
75
- reasoningCount: reasoning.length,
76
- functionCallCount: functionCalls.length
77
- });
78
-
79
- for (const res of reasoning) {
80
- conversation.push(res)
62
+ });
63
+
64
+ throw new LLMError(
65
+ `OpenAI API error: ${error.message}`,
66
+ this.type,
67
+ {
68
+ statusCode: error.status || error.statusCode,
69
+ modelName: input.model
70
+ }
71
+ );
72
+ }
81
73
  }
82
74
 
83
- for (const toolCall of functionCalls) {
75
+ /**
76
+ * Handle a specific tool call from OpenAI response
77
+ * @param {Object} toolCall - The tool call to process
78
+ * @param {Object} state - Current application state
79
+ * @param {Array} conversation - The conversation history
80
+ * @param {Object} toolsAndHandoffsMap - Map of available tools
81
+ */
82
+ async handleToolCall(toolCall, state, conversation, toolsAndHandoffsMap) {
84
83
  try {
85
- const args = JSON.parse(toolCall.arguments)
86
- const name = toolCall.name
84
+ const args = JSON.parse(toolCall.arguments);
85
+ const name = toolCall.name;
87
86
 
88
87
  logger.debug('Executing tool from OpenAI', {
89
88
  toolName: name,
@@ -91,29 +90,10 @@ const onResponse = async function (state, conversation, toolsAndHandoffsMap, res
91
90
  callId: toolCall.call_id
92
91
  });
93
92
 
94
- if (!toolsAndHandoffsMap[name] || !toolsAndHandoffsMap[name].function) {
95
- throw new Error(`Tool "${name}" not found or has no function implementation`);
96
- }
97
-
98
- let result = null
99
- if (toolsAndHandoffsMap[name].type === 'handoff') {
100
- result = await toolsAndHandoffsMap[name].function(conversation, state, args);
101
- } else {
102
- result = await toolsAndHandoffsMap[name].function(state, args);
103
- }
104
- conversation.push(toolCall)
105
- if (toolsAndHandoffsMap[name].type === 'handoff') {
106
- console.log("GPT HANDOFF onResponse", name, result)
107
- const resultParsed = JSON.parse(result)
108
- // Update state with the result
109
- if (resultParsed.session) {
110
- for (const key of Object.keys(resultParsed.session)) {
111
- state[key] = resultParsed.session[key]
112
- }
113
- }
114
- }
93
+ const result = await super.executeToolCall(toolCall, name, args, state, conversation, toolsAndHandoffsMap);
94
+ conversation.push(toolCall);
115
95
 
116
- const resultString = typeof result == 'string' ? result : JSON.stringify(result)
96
+ const resultString = typeof result === 'string' ? result : JSON.stringify(result);
117
97
 
118
98
  logger.debug('Tool execution successful', {
119
99
  toolName: name,
@@ -124,7 +104,7 @@ const onResponse = async function (state, conversation, toolsAndHandoffsMap, res
124
104
  type: "function_call_output",
125
105
  call_id: toolCall.call_id,
126
106
  output: resultString
127
- })
107
+ });
128
108
  } catch (error) {
129
109
  logger.error(`Error executing tool "${toolCall.name}"`, { error });
130
110
 
@@ -137,24 +117,66 @@ const onResponse = async function (state, conversation, toolsAndHandoffsMap, res
137
117
  });
138
118
  }
139
119
  }
140
- return null
141
- }
142
120
 
143
- const prompt = async function (conversation, formattedPrompt) {
144
- logger.debug('Adding user prompt to conversation', {
145
- promptPreview: formattedPrompt.substring(0, 100)
146
- });
147
-
148
- conversation.push({
149
- role: 'user',
150
- content: formattedPrompt
151
- })
121
+ /**
122
+ * Processes the model response, handling text responses and function calls
123
+ * @param {Object} state - Current application state
124
+ * @param {Array} conversation - The conversation history
125
+ * @param {Object} toolsAndHandoffsMap - Map of available tools
126
+ * @param {Object} response - The model response to process
127
+ * @returns {Promise<string|null>} Text response or null if processing tool calls
128
+ */
129
+ async onResponse(state, conversation, toolsAndHandoffsMap, response) {
130
+ if (response.output_text !== undefined && response.output_text.length > 0) {
131
+ logger.debug('OpenAI response contains text, returning directly');
132
+ conversation.push({ role: 'model', parts: [{ text: response.output_text }] });
133
+ return response.output_text;
134
+ }
135
+
136
+ const reasoning = response.output.filter(x => x.type === 'reasoning');
137
+ const functionCalls = response.output.filter(x => x.type === 'function_call');
138
+
139
+ logger.debug('OpenAI response processing', {
140
+ reasoningCount: reasoning.length,
141
+ functionCallCount: functionCalls.length
142
+ });
143
+
144
+ // Add reasoning to conversation
145
+ for (const res of reasoning) {
146
+ conversation.push(res);
147
+ }
148
+
149
+ // Process all tool calls sequentially
150
+ for (const toolCall of functionCalls) {
151
+ await this.handleToolCall(toolCall, state, conversation, toolsAndHandoffsMap);
152
+ }
153
+
154
+ return null;
155
+ }
156
+
157
+ /**
158
+ * Adds a user prompt to the conversation
159
+ * @param {Array} conversation - The conversation history
160
+ * @param {string} formattedPrompt - The formatted user prompt
161
+ * @returns {Promise<void>}
162
+ */
163
+ async prompt(conversation, formattedPrompt) {
164
+ await super.prompt(conversation, formattedPrompt);
165
+
166
+ conversation.push({
167
+ role: 'user',
168
+ content: formattedPrompt
169
+ });
170
+ }
152
171
  }
153
172
 
173
+ // Create a singleton instance
174
+ const openaiLLM = new OpenAILLM();
175
+
154
176
  export default {
155
- type,
156
- getClient,
157
- prompt,
158
- callModel,
159
- onResponse
177
+ type: openaiLLM.type,
178
+ getClient: openaiLLM.getClient.bind(openaiLLM),
179
+ prompt: openaiLLM.prompt.bind(openaiLLM),
180
+ callModel: openaiLLM.callModel.bind(openaiLLM),
181
+ onResponse: openaiLLM.onResponse.bind(openaiLLM)
160
182
  }