agentnet 0.0.1 → 0.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentnet",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "description": "Agent library used by Smartness",
6
6
  "main": "index.js",
@@ -15,7 +15,7 @@
15
15
  "@nats-io/transport-node": "^3.0.2",
16
16
  "colors": "^1.4.0",
17
17
  "openai": "^4.97.0",
18
- "pg": "^8.15.6",
18
+ "pg-promise": "^11.13.0",
19
19
  "redis": "^5.0.1",
20
20
  "uuid": "^11.1.0",
21
21
  "yaml": "^2.7.1"
@@ -22,6 +22,7 @@ const DEFAULT_HOOKS = {
22
22
  const DEFAULT_CONFIG = {
23
23
  metadata: {
24
24
  name: "default",
25
+ namespace: "default",
25
26
  description: "A default agent"
26
27
  },
27
28
  runner: {
@@ -39,9 +40,10 @@ const AGENT_CONFIG_SCHEMA = {
39
40
  type: 'object',
40
41
  properties: {
41
42
  name: { type: 'string' },
43
+ namespace: { type: 'string' },
42
44
  description: { type: 'string' }
43
45
  },
44
- required: ['name']
46
+ required: ['name', 'namespace']
45
47
  },
46
48
  llm: {
47
49
  type: 'object',
@@ -118,6 +120,12 @@ export function Agent() {
118
120
  metadata: config.metadata
119
121
  });
120
122
  }
123
+
124
+ if (!config.metadata.namespace.trim()) {
125
+ throw new ConfigurationError("Agent namespace cannot be empty", {
126
+ metadata: config.metadata
127
+ });
128
+ }
121
129
 
122
130
  // LLM API validation
123
131
  if (typeof config.llm.api !== 'object' || config.llm.api === null) {
@@ -8,7 +8,7 @@ export async function AgentRuntime(agentConfig) {
8
8
  toolsAndHandoffsMap,
9
9
  hooks,
10
10
  store,
11
- metadata: { name: agentName },
11
+ metadata: { name: agentName, namespace },
12
12
  llm: { api: llmApi, config: llmConfig },
13
13
  runner,
14
14
  toolsSchemas: tools,
@@ -21,6 +21,7 @@ export async function AgentRuntime(agentConfig) {
21
21
  // Initialize IO runtime
22
22
  const natsInterfaces = ioInterfaces.filter(x => x.type === 'NatsIO')
23
23
  const { handleTask, discoveredAgents } = await NatsIOAgentRuntime(
24
+ namespace,
24
25
  agentName,
25
26
  natsInterfaces,
26
27
  discoverySchemas
@@ -94,7 +95,6 @@ export async function AgentRuntime(agentConfig) {
94
95
 
95
96
  // Execute agent runtime
96
97
  const result = await taskFunction(storeState.state, storeState.conversation, promptContent);
97
-
98
98
  // Process result through response hook
99
99
  const responseMessage = await response(storeState.state, storeState.conversation, result);
100
100
  // Save session state and session data
@@ -16,10 +16,83 @@ const HEARTBEAT_INTERVAL = 1000;
16
16
  const TIMEOUT_TASK_REQUEST = 60000;
17
17
  const MAX_RECONNECT_ATTEMPTS = 5;
18
18
 
19
+ /**
20
+ * Class representing a discovery message for agent capabilities
21
+ */
22
+ class DiscoveryMessage {
23
+ /**
24
+ * Create a new discovery message
25
+ * @param {string} namespace - The agent namespace
26
+ * @param {string} agentName - The agent name
27
+ * @param {Array} schemas - The agent capability schemas
28
+ */
29
+ constructor(namespace, agentName, schemas) {
30
+ if (!namespace) throw new Error('Namespace is required');
31
+ if (!agentName) throw new Error('Agent name is required');
32
+ if (!Array.isArray(schemas)) throw new Error('Schemas must be an array');
33
+
34
+ this.type = 'discovery';
35
+ this.network = `${namespace}.${agentName}`;
36
+ this.agentName = agentName;
37
+ this.schemas = schemas;
38
+ }
39
+
40
+ /**
41
+ * Serialize the message to a JSON string
42
+ * @returns {string} The serialized message
43
+ */
44
+ serialize() {
45
+ return JSON.stringify({
46
+ type: this.type,
47
+ network: this.network,
48
+ agentName: this.agentName,
49
+ schemas: this.schemas
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Create a DiscoveryMessage from a serialized string
55
+ * @param {string} data - The serialized message data
56
+ * @returns {DiscoveryMessage} A new DiscoveryMessage instance
57
+ */
58
+ static fromString(data) {
59
+ const payload = JSON.parse(data);
60
+
61
+ if (payload.type !== 'discovery') {
62
+ throw new Error('Not a discovery message');
63
+ }
64
+
65
+ // Extract namespace from network (format: namespace.agentName)
66
+ const networkParts = payload.network.split('.');
67
+ if (networkParts.length !== 2) {
68
+ throw new Error('Invalid network format in discovery message');
69
+ }
70
+
71
+ const namespace = networkParts[0];
72
+ return new DiscoveryMessage(namespace, payload.agentName, payload.schemas);
73
+ }
74
+
75
+ /**
76
+ * Validate if a payload conforms to discovery message structure
77
+ * @param {Object} payload - The payload to validate
78
+ * @returns {boolean} Whether the payload is valid
79
+ */
80
+ static isValid(payload) {
81
+ return (
82
+ payload &&
83
+ typeof payload === 'object' &&
84
+ payload.type === 'discovery' &&
85
+ typeof payload.network === 'string' &&
86
+ typeof payload.agentName === 'string' &&
87
+ Array.isArray(payload.schemas)
88
+ );
89
+ }
90
+ }
91
+
19
92
  /**
20
93
  * Sets up discovery subscription to find other agents
21
94
  */
22
- async function setupDiscoverySubscription(nc, discoveryTopic, agentName, discoveredAgents) {
95
+ async function setupDiscoverySubscription(nc, discoveryTopic, namespace, agentName, discoveredAgents, acceptedNetworks) {
23
96
  let discoverySub;
24
97
 
25
98
  try {
@@ -35,27 +108,84 @@ async function setupDiscoverySubscription(nc, discoveryTopic, agentName, discove
35
108
 
36
109
  const handleDiscovery = async () => {
37
110
  try {
111
+ let nonAcceptedNetworks = {}
38
112
  for await (const m of discoverySub) {
39
113
  try {
40
- const payloadSetup = JSON.parse(m.string());
41
-
42
- // Validate the payload structure
43
- if (!payloadSetup.agentName || !Array.isArray(payloadSetup.schemas)) {
44
- logger.warn('Invalid discovery payload received', { payload: payloadSetup });
114
+ // Attempt to parse and validate the discovery message
115
+ let discoveryMessage;
116
+ try {
117
+ discoveryMessage = DiscoveryMessage.fromString(m.string());
118
+ } catch (parseError) {
119
+ logger.warn('Invalid discovery message format', { error: parseError.message });
120
+ continue;
121
+ }
122
+
123
+ const network = discoveryMessage.network;
124
+ const networkNamespace = network.split(".")[0];
125
+ const networkName = network.split(".")[1];
126
+
127
+ // Skip self
128
+ if (network === `${namespace}.${agentName}`) {
129
+ continue;
130
+ }
131
+
132
+ // Skip if already processed
133
+ if (nonAcceptedNetworks[network] === true) {
45
134
  continue;
46
135
  }
136
+
137
+ let isAccepted = false;
138
+ for (const acceptedNetwork of acceptedNetworks) {
139
+ const acceptedNetworkNamespace = acceptedNetwork.split(".")[0];
140
+ const acceptedNetworkName = acceptedNetwork.split(".")[1];
141
+
142
+ if (acceptedNetworkNamespace === networkNamespace && acceptedNetworkName === networkName) {
143
+ isAccepted = true;
144
+ continue;
145
+ }
146
+ // Check for wildcard patterns in accepted networks
147
+
148
+ if (acceptedNetworkNamespace === '*' && acceptedNetworkName === '*') {
149
+ // Both namespace and name are wildcards, accept any network
150
+ logger.debug(`Agent ${agentName} accepting network ${network} due to wildcard pattern *.*`);
151
+ isAccepted = true;
152
+ continue;
153
+ }
154
+
155
+ if (acceptedNetworkNamespace === '*' && acceptedNetworkName === networkName) {
156
+ // Namespace is wildcard, but name matches
157
+ logger.debug(`Agent ${agentName} accepting network ${network} due to wildcard pattern *.${networkName}`);
158
+ isAccepted = true;
159
+ continue;
160
+ }
161
+
162
+ if (acceptedNetworkNamespace === networkNamespace && acceptedNetworkName === '*') {
163
+ // Name is wildcard, but namespace matches
164
+ logger.debug(`Agent ${agentName} accepting network ${network} due to wildcard pattern ${networkNamespace}.*`);
165
+ isAccepted = true;
166
+ continue;
167
+ }
168
+ }
169
+
170
+ // Skip if not accepted
171
+ if (!isAccepted) {
172
+ logger.warn(`Agent ${agentName} does not accept network ${network}`);
173
+ nonAcceptedNetworks[network] = true;
174
+ continue;
175
+ }
47
176
 
48
- for (const schema of payloadSetup.schemas) {
177
+ // Process the schemas from the discovery message
178
+ for (const schema of discoveryMessage.schemas) {
49
179
  // Skip invalid schemas
50
180
  if (!schema || !schema.name) {
51
181
  logger.warn('Invalid schema in discovery payload', { schema });
52
182
  continue;
53
183
  }
54
184
 
55
- const agentKey = `${payloadSetup.agentName}-${schema.name}`;
185
+ const agentKey = `${network}-${schema.name}`;
56
186
 
57
- if (payloadSetup.agentName !== agentName && !discoveredAgents[agentKey]) {
58
- logger.info(`${agentName} discovered agent capability: ${payloadSetup.agentName} with capability ${schema.name}`);
187
+ if (discoveryMessage.agentName !== agentName && !discoveredAgents[agentKey]) {
188
+ logger.info(`${agentName} discovered agent capability: ${discoveryMessage.agentName} with capability ${schema.name}`);
59
189
 
60
190
  const handoffFunction = async (conversation, state, input) => {
61
191
  try {
@@ -70,7 +200,7 @@ async function setupDiscoverySubscription(nc, discoveryTopic, agentName, discove
70
200
  content: input
71
201
  })
72
202
  const req = await nc.request(
73
- payloadSetup.agentName,
203
+ discoveryMessage.agentName,
74
204
  message.serialize(),
75
205
  { timeout: TIMEOUT_TASK_REQUEST }
76
206
  );
@@ -79,7 +209,7 @@ async function setupDiscoverySubscription(nc, discoveryTopic, agentName, discove
79
209
  {
80
210
  maxRetries: 2,
81
211
  onRetry: ({ attempt }) => {
82
- logger.warn(`Retrying handoff attempt ${attempt} to ${payloadSetup.agentName}`, {
212
+ logger.warn(`Retrying handoff attempt ${attempt} to ${discoveryMessage.agentName}`, {
83
213
  schema: schema.name
84
214
  });
85
215
  }
@@ -88,18 +218,18 @@ async function setupDiscoverySubscription(nc, discoveryTopic, agentName, discove
88
218
  return response;
89
219
  } catch (error) {
90
220
  throw new HandoffError(
91
- `Handoff to agent ${payloadSetup.agentName} failed: ${error.message}`,
221
+ `Handoff to agent ${discoveryMessage.agentName} failed: ${error.message}`,
92
222
  agentName,
93
- payloadSetup.agentName,
223
+ discoveryMessage.agentName,
94
224
  { schemaName: schema.name }
95
225
  );
96
226
  }
97
227
  },
98
228
  TIMEOUT_TASK_REQUEST,
99
- `handoff to ${payloadSetup.agentName}`
229
+ `handoff to ${discoveryMessage.agentName}`
100
230
  );
101
231
  } catch (error) {
102
- logger.error(`Handoff error to ${payloadSetup.agentName}`, {
232
+ logger.error(`Handoff error to ${discoveryMessage.agentName}`, {
103
233
  error,
104
234
  schema: schema.name
105
235
  });
@@ -141,18 +271,13 @@ async function setupDiscoverySubscription(nc, discoveryTopic, agentName, discove
141
271
  /**
142
272
  * Sets up a heartbeat to announce this agent's capabilities
143
273
  */
144
- function setupDiscoveryHeartbeat(nc, discoveryTopic, agentName, discoverySchemas) {
274
+ function setupDiscoveryHeartbeat(nc, discoveryTopic, namespace, agentName, discoverySchemas) {
145
275
  let consecutiveErrors = 0;
146
276
 
147
277
  return setInterval(async () => {
148
278
  try {
149
- const heartbeatPayload = {
150
- type: 'discovery',
151
- agentName: agentName,
152
- schemas: discoverySchemas
153
- };
154
-
155
- await nc.publish(discoveryTopic, JSON.stringify(heartbeatPayload));
279
+ const discoveryMessage = new DiscoveryMessage(namespace, agentName, discoverySchemas);
280
+ await nc.publish(discoveryTopic, discoveryMessage.serialize());
156
281
 
157
282
  // Reset error counter on success
158
283
  if (consecutiveErrors > 0) {
@@ -206,6 +331,7 @@ async function createTaskHandler(nc, agentName, processingFunction) {
206
331
  if (!payload || typeof payload !== 'object') {
207
332
  throw new Error('Invalid payload: not a JSON object');
208
333
  }
334
+
209
335
  const message = new Message(payload)
210
336
  const input = message.getContent()
211
337
  const session = message.getSession()
@@ -309,7 +435,7 @@ async function safeConnect(instance, options = {}) {
309
435
  /**
310
436
  * Creates a NATS-based runtime for agent communication
311
437
  */
312
- export async function NatsIOAgentRuntime(agentName, ioInterfaces, discoverySchemas) {
438
+ export async function NatsIOAgentRuntime(namespace, agentName, ioInterfaces, discoverySchemas) {
313
439
  if (ioInterfaces.length > 1) {
314
440
  throw new TransportError(
315
441
  'Only one IO Nats interface is supported',
@@ -342,13 +468,14 @@ export async function NatsIOAgentRuntime(agentName, ioInterfaces, discoverySchem
342
468
  }
343
469
 
344
470
  const discoveryTopic = io.config.bindings.discoveryTopic;
471
+ const acceptedNetworks = io.config.bindings.acceptedNetworks || [];
345
472
  logger.info(`Agent ${agentName} initialized with discovery topic ${discoveryTopic}`);
346
473
 
347
474
  // Step 1. Subscribe to discovery topic
348
- await setupDiscoverySubscription(nc, discoveryTopic, agentName, discoveredAgents);
475
+ await setupDiscoverySubscription(nc, discoveryTopic, namespace, agentName, discoveredAgents, acceptedNetworks);
349
476
 
350
477
  // Step 2. Publish discovery heartbeat
351
- const interval = setupDiscoveryHeartbeat(nc, discoveryTopic, agentName, discoverySchemas);
478
+ const interval = setupDiscoveryHeartbeat(nc, discoveryTopic, namespace, agentName, discoverySchemas);
352
479
  intervals.push(interval);
353
480
 
354
481
  // Step 3. Create task handler
package/src/llm/gemini.js CHANGED
@@ -97,8 +97,13 @@ const onResponse = async function (state, conversation, toolsAndHandoffsMap, res
97
97
  if (!toolsAndHandoffsMap[name] || !toolsAndHandoffsMap[name].function) {
98
98
  throw new Error(`Tool "${name}" not found or has no function implementation`);
99
99
  }
100
-
101
- let result = await toolsAndHandoffsMap[name].function(conversation, state, args);
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
+ }
102
107
  if (toolsAndHandoffsMap[name].type === 'handoff') {
103
108
  const resultParsed = JSON.parse(result)
104
109
  // Update state with the result
package/src/llm/gpt.js CHANGED
@@ -95,7 +95,12 @@ const onResponse = async function (state, conversation, toolsAndHandoffsMap, res
95
95
  throw new Error(`Tool "${name}" not found or has no function implementation`);
96
96
  }
97
97
 
98
- let result = await toolsAndHandoffsMap[name].function(conversation, state, args)
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
+ }
99
104
  conversation.push(toolCall)
100
105
  if (toolsAndHandoffsMap[name].type === 'handoff') {
101
106
  console.log("GPT HANDOFF onResponse", name, result)
@@ -1,31 +1,6 @@
1
1
  import { v4 as uuid } from 'uuid'
2
2
  import { createClient } from 'redis'
3
- import pg from 'pg'
4
-
5
- let config = {
6
- user: process.env.PG_USER || 'postgres',
7
- host: process.env.PG_HOST || 'localhost',
8
- database: process.env.PG_DATABASE || 'postgres',
9
- password: process.env.PG_PASSWORD || 'password',
10
- port: process.env.PG_PORT || 5433
11
- }
12
-
13
- if (process.env.PG_USE_SSL == 'true' || process.env.PG_USE_SSL == true) {
14
- config.ssl = {
15
- rejectUnauthorized: false
16
- }
17
- }
18
-
19
- const pool = new pg.Pool(config)
20
-
21
- pool.on('error', err => {
22
- console.log(new Date(), 'Lost Postgres connection', err)
23
- })
24
-
25
- export async function getClient() {
26
- const client = await pool.connect()
27
- return client
28
- }
3
+ import pgp from 'pg-promise'
29
4
 
30
5
  export function session (id) {
31
6
  let state = {}
@@ -117,37 +92,96 @@ export function redisStore (_config = null) {
117
92
  }
118
93
  }
119
94
 
120
- export function postgresStore (_config = null) {
121
- let client = null
122
- let config = _config
95
+ export function postgresStore(config = null) {
96
+ let db = null;
97
+ const pgPromise = pgp(); // Initialize pg-promise
98
+ // Default connection config
99
+ const defaultConfig = {
100
+ host: process.env.PG_HOST || 'localhost',
101
+ port: process.env.PG_PORT || 5433,
102
+ database: process.env.PG_DATABASE || 'postgres',
103
+ user: process.env.PG_USER || 'postgres',
104
+ password: process.env.PG_PASSWORD || 'password',
105
+ max: 30, // max number of clients in the pool
106
+ ssl: process.env.PG_USE_SSL === 'true' ? { rejectUnauthorized: false } : null,
107
+ table: 'conversation_state',
108
+ schema: 'smartchat_agent'
109
+ };
110
+
111
+ const connectionConfig = config || defaultConfig;
123
112
 
124
113
  return {
125
- connect: async function () {
126
- if (!client) {
127
- client = await getClient()
114
+ connect: async function() {
115
+ if (!db) {
116
+ // For URL-style connection string
117
+ if (typeof connectionConfig === 'string') {
118
+ db = pgPromise(connectionConfig);
119
+ } else {
120
+ db = pgPromise(connectionConfig);
121
+ }
122
+
123
+ // Test connection
124
+ try {
125
+ await db.connect();
126
+
127
+ // Create schema if not exists
128
+ await db.none('CREATE SCHEMA IF NOT EXISTS $1:name', [connectionConfig.schema]);
129
+
130
+ // Create table if not exists
131
+ await db.none(`
132
+ CREATE TABLE IF NOT EXISTS $1:name.$2:name (
133
+ id UUID PRIMARY KEY,
134
+ state_id TEXT,
135
+ state TEXT,
136
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
137
+ )
138
+ `, [connectionConfig.schema, connectionConfig.table]);
139
+
140
+ // Create unique index if not exists
141
+ await db.none(`
142
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_acc_agent_chat_id
143
+ ON $1:name.$2:name(state_id)
144
+ `, [connectionConfig.schema, connectionConfig.table]);
145
+
146
+ } catch (error) {
147
+ throw error;
148
+ }
128
149
  }
129
150
  },
130
- disconnect: async function () {
131
- if (client) {
132
- await client.end()
133
- client = null
151
+
152
+ disconnect: async function() {
153
+ if (db) {
154
+ await pgPromise.end();
155
+ db = null;
134
156
  }
135
157
  },
136
- set: async function (key, value) {
137
- const id = uuid()
138
- return await client.query('INSERT INTO smartchat_agent.conversation_state (state_id, state, id) VALUES ($1,$2,$3) ON CONFLICT (state_id) DO UPDATE SET state_id=$1, state=$2', [key, value, id])
139
- },
140
- get: async function (key) {
141
- const res = await client.query('SELECT state FROM smartchat_agent.conversation_state WHERE state_id=$1', [key])
142
- if (res.rows.length == 1) {
143
- return res.rows[0].state
158
+
159
+ set: async function(key, value) {
160
+ const id = uuid();
161
+ try {
162
+ return await db.one(
163
+ 'INSERT INTO $1:name.$2:name (state_id, state, id) VALUES ($3, $4, $5) ON CONFLICT (state_id) DO UPDATE SET state=$4, updated_at=CURRENT_TIMESTAMP RETURNING id',
164
+ [connectionConfig.schema, connectionConfig.table, key, value, id]
165
+ );
166
+ } catch (error) {
167
+ console.error('Error storing state:', error);
168
+ throw error;
144
169
  }
145
- if (res.rows.length > 1) {
146
- throw 'Something went wrong at pg store'
170
+ },
171
+
172
+ get: async function(key) {
173
+ try {
174
+ const result = await db.oneOrNone(
175
+ 'SELECT state FROM $1:name.$2:name WHERE state_id = $3',
176
+ [connectionConfig.schema, connectionConfig.table, key]
177
+ );
178
+ return result ? result.state : null;
179
+ } catch (error) {
180
+ console.error('Error retrieving state:', error);
181
+ throw error;
147
182
  }
148
- return null
149
183
  }
150
- }
184
+ };
151
185
  }
152
186
 
153
187
  export function memoryStore () {