agentnet 0.0.1 → 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,155 +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
+ }
83
+
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
+ }
75
118
 
76
- const onResponse = async function (state, conversation, toolsAndHandoffsMap, response) {
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 = await toolsAndHandoffsMap[name].function(conversation, state, args);
102
- if (toolsAndHandoffsMap[name].type === 'handoff') {
103
- const resultParsed = JSON.parse(result)
104
- // Update state with the result
105
- if (resultParsed.session) {
106
- for (const key of Object.keys(resultParsed.session)) {
107
- state[key] = resultParsed.session[key]
108
- }
109
- }
110
- }
111
-
112
- const function_response_part = {
113
- name: name,
114
- response: typeof result === 'string' ? { answer: result } : result
115
- };
116
-
117
- // Append function call and result of the function execution to contents
118
- conversation.push({ role: 'model', parts: [{ functionCall: toolCall }] });
119
- conversation.push({ role: 'user', parts: [{ functionResponse: function_response_part }] });
120
-
121
- logger.debug('Tool execution successful', { toolName: name });
122
- } catch (error) {
123
- logger.error(`Error executing tool "${name}"`, { error });
124
- // Return error as function response
125
- const errorResponse = {
126
- name: name,
127
- response: { error: error.message }
128
- };
129
-
130
- conversation.push({ role: 'model', parts: [{ functionCall: toolCall }] });
131
- conversation.push({ role: 'user', parts: [{ functionResponse: errorResponse }] });
132
- }
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
+ }
133
145
  }
134
146
 
135
147
  return null;
136
- }
148
+ }
137
149
 
138
- const prompt = async function (conversation, formattedPrompt) {
139
- logger.debug('Adding user prompt to conversation', {
140
- promptPreview: formattedPrompt.substring(0, 100)
141
- });
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);
142
158
 
143
159
  conversation.push({
144
- role: 'user',
145
- parts: [{ text: formattedPrompt}]
160
+ role: 'user',
161
+ parts: [{ text: formattedPrompt }]
146
162
  });
163
+ }
147
164
  }
148
165
 
166
+ // Create a singleton instance
167
+ const geminiLLM = new GeminiLLM();
168
+
149
169
  export default {
150
- type,
151
- getClient,
152
- prompt,
153
- callModel,
154
- 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)
155
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,24 +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 = await toolsAndHandoffsMap[name].function(conversation, state, args)
99
- conversation.push(toolCall)
100
- if (toolsAndHandoffsMap[name].type === 'handoff') {
101
- console.log("GPT HANDOFF onResponse", name, result)
102
- const resultParsed = JSON.parse(result)
103
- // Update state with the result
104
- if (resultParsed.session) {
105
- for (const key of Object.keys(resultParsed.session)) {
106
- state[key] = resultParsed.session[key]
107
- }
108
- }
109
- }
93
+ const result = await super.executeToolCall(toolCall, name, args, state, conversation, toolsAndHandoffsMap);
94
+ conversation.push(toolCall);
110
95
 
111
- const resultString = typeof result == 'string' ? result : JSON.stringify(result)
96
+ const resultString = typeof result === 'string' ? result : JSON.stringify(result);
112
97
 
113
98
  logger.debug('Tool execution successful', {
114
99
  toolName: name,
@@ -119,7 +104,7 @@ const onResponse = async function (state, conversation, toolsAndHandoffsMap, res
119
104
  type: "function_call_output",
120
105
  call_id: toolCall.call_id,
121
106
  output: resultString
122
- })
107
+ });
123
108
  } catch (error) {
124
109
  logger.error(`Error executing tool "${toolCall.name}"`, { error });
125
110
 
@@ -132,24 +117,66 @@ const onResponse = async function (state, conversation, toolsAndHandoffsMap, res
132
117
  });
133
118
  }
134
119
  }
135
- return null
136
- }
137
120
 
138
- const prompt = async function (conversation, formattedPrompt) {
139
- logger.debug('Adding user prompt to conversation', {
140
- promptPreview: formattedPrompt.substring(0, 100)
141
- });
142
-
143
- conversation.push({
144
- role: 'user',
145
- content: formattedPrompt
146
- })
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
+ }
147
171
  }
148
172
 
173
+ // Create a singleton instance
174
+ const openaiLLM = new OpenAILLM();
175
+
149
176
  export default {
150
- type,
151
- getClient,
152
- prompt,
153
- callModel,
154
- 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)
155
182
  }