agentnet 0.1.1 → 0.1.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/README.md +1 -1
- package/docs/conversation.md +92 -0
- package/examples/conversation-usage.js +68 -0
- package/examples/customer-support/index.js +3 -3
- package/examples/event-planner/index.js +4 -4
- package/examples/smartness/index.js +2 -2
- package/package.json +1 -1
- package/src/agent/client.js +7 -3
- package/src/agent/executor.js +2 -2
- package/src/index.js +4 -1
- package/src/llm/base.js +13 -6
- package/src/llm/gemini.js +49 -11
- package/src/llm/gpt.js +66 -17
- package/src/store/store.js +30 -20
- package/src/transport/nats.js +2 -2
- package/src/utils/conversation.js +178 -0
package/README.md
CHANGED
|
@@ -267,7 +267,7 @@ const message = new Message({
|
|
|
267
267
|
|
|
268
268
|
// Query the agent using the client
|
|
269
269
|
console.log("Sending query to the entrypoint agent...");
|
|
270
|
-
const response = await client.queryIo(natsIO, 'smartchat
|
|
270
|
+
const response = await client.queryIo(natsIO, 'smartchat', 'entrypointAgent', message);
|
|
271
271
|
|
|
272
272
|
// Process the response
|
|
273
273
|
console.log("Agent Response:", response.getContent());
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Conversation Class
|
|
2
|
+
|
|
3
|
+
The `Conversation` class manages conversation history with additional metadata for each message. It provides an improved way to handle conversations compared to a plain array, allowing for more sophisticated conversation management, including proper trimming based on message types.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Store messages with additional metadata (type, timestamp)
|
|
8
|
+
- Proper conversation trimming (e.g., keeping at least one user message)
|
|
9
|
+
- Support for different message types (user inputs, model responses, function calls)
|
|
10
|
+
- Easy serialization/deserialization
|
|
11
|
+
- Backward compatibility with existing code
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { Conversation } from 'smartagent';
|
|
17
|
+
|
|
18
|
+
// Create a new conversation
|
|
19
|
+
const conversation = new Conversation();
|
|
20
|
+
|
|
21
|
+
// Add messages
|
|
22
|
+
conversation.addUserMessage({
|
|
23
|
+
role: 'user',
|
|
24
|
+
content: 'Hello, how can you help me?'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
conversation.addModelResponse({
|
|
28
|
+
role: 'assistant',
|
|
29
|
+
content: 'I can help with many tasks. What do you need?'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Get raw conversation for LLM APIs
|
|
33
|
+
const rawConversation = conversation.getRawConversation();
|
|
34
|
+
|
|
35
|
+
// Get all messages with metadata
|
|
36
|
+
const allMessages = conversation.getMessages();
|
|
37
|
+
|
|
38
|
+
// Trim conversation while ensuring it starts with a user message
|
|
39
|
+
conversation.trim(10);
|
|
40
|
+
|
|
41
|
+
// Import from existing array
|
|
42
|
+
conversation.importFromArray(existingArray);
|
|
43
|
+
|
|
44
|
+
// Serialize/deserialize
|
|
45
|
+
const serialized = conversation.serialize();
|
|
46
|
+
conversation.deserialize(serialized);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Integration with SessionStore
|
|
50
|
+
|
|
51
|
+
The `SessionStore` class has been updated to use the `Conversation` class internally:
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
import { SessionStore } from 'smartagent';
|
|
55
|
+
|
|
56
|
+
const sessionStore = new SessionStore('session-id');
|
|
57
|
+
|
|
58
|
+
// Load the conversation
|
|
59
|
+
await sessionStore.load(store);
|
|
60
|
+
|
|
61
|
+
// Get the conversation manager
|
|
62
|
+
const conversationManager = sessionStore.getConversationManager();
|
|
63
|
+
|
|
64
|
+
// Add a message
|
|
65
|
+
conversationManager.addUserMessage({ /* message */ });
|
|
66
|
+
|
|
67
|
+
// Trim with proper handling
|
|
68
|
+
sessionStore.trimConversation(10);
|
|
69
|
+
|
|
70
|
+
// Save conversation
|
|
71
|
+
await sessionStore.dump(store);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Message Types
|
|
75
|
+
|
|
76
|
+
The Conversation class tracks different message types:
|
|
77
|
+
|
|
78
|
+
- `user_input`: Messages from the user
|
|
79
|
+
- `model_response`: Responses from the LLM model
|
|
80
|
+
- `function_call`: Function/tool calls from the model
|
|
81
|
+
- `function_result`: Results of function/tool calls
|
|
82
|
+
|
|
83
|
+
## Compatibility
|
|
84
|
+
|
|
85
|
+
The implementation maintains backward compatibility with existing code that uses plain arrays for conversations. The LLM implementations (`GeminiLLM` and `OpenAILLM`) have been updated to properly handle both plain arrays and `Conversation` objects.
|
|
86
|
+
|
|
87
|
+
## Benefits
|
|
88
|
+
|
|
89
|
+
- **Better trimming**: Trimming maintains conversation context by ensuring at least one user message remains
|
|
90
|
+
- **Type awareness**: Messages are tracked by type, making it easier to understand the conversation flow
|
|
91
|
+
- **Metadata**: Additional information can be stored with each message
|
|
92
|
+
- **Serialization**: Easy serialization and deserialization for storage
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Conversation, SessionStore } from '../src/index.js';
|
|
2
|
+
import { MemoryStore } from '../src/utils/store.js';
|
|
3
|
+
|
|
4
|
+
// Example of using the Conversation class
|
|
5
|
+
async function conversationExample() {
|
|
6
|
+
// Create a new conversation
|
|
7
|
+
const conversation = new Conversation();
|
|
8
|
+
|
|
9
|
+
// Add user and model messages
|
|
10
|
+
conversation.addUserMessage({
|
|
11
|
+
role: 'user',
|
|
12
|
+
content: 'Hello, I need help with my project.'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
conversation.addModelResponse({
|
|
16
|
+
role: 'assistant',
|
|
17
|
+
content: 'I\'d be happy to help! What kind of project are you working on?'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Get the raw conversation (format needed by LLMs)
|
|
21
|
+
const rawConversation = conversation.getRawConversation();
|
|
22
|
+
console.log('Raw conversation:', JSON.stringify(rawConversation, null, 2));
|
|
23
|
+
|
|
24
|
+
// Get full messages with metadata
|
|
25
|
+
const messages = conversation.getMessages();
|
|
26
|
+
console.log('Messages with metadata:', JSON.stringify(messages, null, 2));
|
|
27
|
+
|
|
28
|
+
// Trim the conversation
|
|
29
|
+
conversation.trim(5);
|
|
30
|
+
console.log('After trimming:', JSON.stringify(conversation.getMessages(), null, 2));
|
|
31
|
+
|
|
32
|
+
// Import from existing array
|
|
33
|
+
const existingConversation = [
|
|
34
|
+
{ role: 'user', content: 'What is the weather today?' },
|
|
35
|
+
{ role: 'assistant', content: 'I don\'t have access to real-time weather data.' },
|
|
36
|
+
{ role: 'user', content: 'Can you help me with coding instead?' }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
conversation.importFromArray(existingConversation);
|
|
40
|
+
console.log('After importing:', JSON.stringify(conversation.getRawConversation(), null, 2));
|
|
41
|
+
|
|
42
|
+
// SessionStore example
|
|
43
|
+
const sessionStore = new SessionStore('example-session');
|
|
44
|
+
const store = new MemoryStore();
|
|
45
|
+
|
|
46
|
+
// Set conversation and state
|
|
47
|
+
sessionStore.setConversation(conversation);
|
|
48
|
+
sessionStore.setState({ userId: '12345', lastActivity: new Date().toISOString() });
|
|
49
|
+
|
|
50
|
+
// Trim conversation to keep only the latest 2 messages
|
|
51
|
+
sessionStore.trimConversation(2);
|
|
52
|
+
console.log('After trimming in SessionStore:',
|
|
53
|
+
JSON.stringify(sessionStore.getConversation(), null, 2));
|
|
54
|
+
|
|
55
|
+
// Save to store
|
|
56
|
+
await sessionStore.dump(store);
|
|
57
|
+
|
|
58
|
+
// Load from store
|
|
59
|
+
const newSessionStore = new SessionStore('example-session');
|
|
60
|
+
await newSessionStore.load(store);
|
|
61
|
+
|
|
62
|
+
console.log('Loaded conversation:',
|
|
63
|
+
JSON.stringify(newSessionStore.getConversation(), null, 2));
|
|
64
|
+
console.log('Loaded state:',
|
|
65
|
+
JSON.stringify(newSessionStore.getState(), null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
conversationExample().catch(console.error);
|
|
@@ -349,7 +349,7 @@ async function main() {
|
|
|
349
349
|
|
|
350
350
|
// Start with the triage agent
|
|
351
351
|
const client = AgentClient();
|
|
352
|
-
const res = await client.queryIo(natsIO, 'customerSupport
|
|
352
|
+
const res = await client.queryIo(natsIO, 'customerSupport', 'triageAgent', new Message({
|
|
353
353
|
content: customerQuery,
|
|
354
354
|
session: {
|
|
355
355
|
id: caseId,
|
|
@@ -363,7 +363,7 @@ async function main() {
|
|
|
363
363
|
|
|
364
364
|
// For demonstration purposes, let's simulate a complete flow through the technical agent
|
|
365
365
|
console.log("\nRouting to Technical Agent...");
|
|
366
|
-
const res2 = await client.queryIo(natsIO, 'customerSupport
|
|
366
|
+
const res2 = await client.queryIo(natsIO, 'customerSupport', 'technicalAgent', new Message({
|
|
367
367
|
content: customerQuery,
|
|
368
368
|
session: {
|
|
369
369
|
id: caseId,
|
|
@@ -388,7 +388,7 @@ async function main() {
|
|
|
388
388
|
|
|
389
389
|
// Now follow up with the customer
|
|
390
390
|
console.log("\nFollowing up with customer after resolution...");
|
|
391
|
-
const res3 = await client.queryIo(natsIO, 'customerSupport
|
|
391
|
+
const res3 = await client.queryIo(natsIO, 'customerSupport', 'followupAgent', new Message({
|
|
392
392
|
content: `Please follow up on case ${caseId}`,
|
|
393
393
|
session: {
|
|
394
394
|
id: caseId
|
|
@@ -479,7 +479,7 @@ async function main() {
|
|
|
479
479
|
const createRequest = "I need to schedule a design review meeting next Monday at 2pm for 1 hour with the design team. Today is 11th May 2025.";
|
|
480
480
|
|
|
481
481
|
console.log(`User request: "${createRequest}"`);
|
|
482
|
-
const createResponse = await client.queryIo(natsIO, 'eventPlanner
|
|
482
|
+
const createResponse = await client.queryIo(natsIO, 'eventPlanner', 'plannerAgent', new Message({
|
|
483
483
|
content: createRequest,
|
|
484
484
|
session: {
|
|
485
485
|
id: uuidv4(),
|
|
@@ -495,7 +495,7 @@ async function main() {
|
|
|
495
495
|
const searchRequest = "Show me all meetings with Bob next month. Today is october 2023.";
|
|
496
496
|
|
|
497
497
|
console.log(`User request: "${searchRequest}"`);
|
|
498
|
-
const searchResponse = await client.queryIo(natsIO, 'eventPlanner
|
|
498
|
+
const searchResponse = await client.queryIo(natsIO, 'eventPlanner', 'eventFinderAgent', new Message({
|
|
499
499
|
content: searchRequest,
|
|
500
500
|
session: {
|
|
501
501
|
id: uuidv4(),
|
|
@@ -511,7 +511,7 @@ async function main() {
|
|
|
511
511
|
const updateRequest = "Change the Weekly Team Meeting to start at 9:30am instead of 10am.";
|
|
512
512
|
|
|
513
513
|
console.log(`User request: "${updateRequest}"`);
|
|
514
|
-
const updateResponse = await client.queryIo(natsIO, 'eventPlanner
|
|
514
|
+
const updateResponse = await client.queryIo(natsIO, 'eventPlanner', 'plannerAgent', new Message({
|
|
515
515
|
content: updateRequest,
|
|
516
516
|
session: {
|
|
517
517
|
id: uuidv4(),
|
|
@@ -527,7 +527,7 @@ async function main() {
|
|
|
527
527
|
const conflictRequest = "Do I have any scheduling conflicts next week?";
|
|
528
528
|
|
|
529
529
|
console.log(`User request: "${conflictRequest}"`);
|
|
530
|
-
const conflictResponse = await client.queryIo(natsIO, 'eventPlanner
|
|
530
|
+
const conflictResponse = await client.queryIo(natsIO, 'eventPlanner', 'eventFinderAgent', new Message({
|
|
531
531
|
content: conflictRequest,
|
|
532
532
|
session: {
|
|
533
533
|
id: uuidv4(),
|
|
@@ -62,7 +62,7 @@ const message = new Message({
|
|
|
62
62
|
id: sessionId
|
|
63
63
|
}
|
|
64
64
|
})
|
|
65
|
-
const res = await agentClient.queryIo(io, 'smartexample
|
|
65
|
+
const res = await agentClient.queryIo(io, 'smartexample', 'entrypoint', message)
|
|
66
66
|
console.log("=======\n", res.getContent())
|
|
67
67
|
console.log("=======\n", res.getSession())
|
|
68
68
|
|
|
@@ -72,6 +72,6 @@ const message2 = new Message({
|
|
|
72
72
|
id: sessionId
|
|
73
73
|
}
|
|
74
74
|
})
|
|
75
|
-
const res2 = await agentClient.queryIo(io, 'smartexample
|
|
75
|
+
const res2 = await agentClient.queryIo(io, 'smartexample', 'entrypoint', message2)
|
|
76
76
|
console.log("=======\n", res2.getContent())
|
|
77
77
|
console.log("=======\n", res2.getSession())
|
package/package.json
CHANGED
package/src/agent/client.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Message, Response } from '../index.js'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
export function AgentClient(config = {}) {
|
|
4
|
+
const requestTimeout = config.requestTimeout || 120000
|
|
5
|
+
|
|
3
6
|
return {
|
|
4
7
|
queryAgent: async (agent, input) => {
|
|
5
8
|
return await agent.query(input)
|
|
6
9
|
},
|
|
7
10
|
|
|
8
|
-
queryIo: async (io,
|
|
11
|
+
queryIo: async (io, namespace, name, message) => {
|
|
9
12
|
const transport = await io.connect()
|
|
10
|
-
const
|
|
13
|
+
const target = namespace + '.' + name
|
|
14
|
+
const response = await transport.request(target, message.serialize(), { timeout: requestTimeout })
|
|
11
15
|
return new Response(JSON.parse(response.string()))
|
|
12
16
|
}
|
|
13
17
|
}
|
package/src/agent/executor.js
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
withRetry
|
|
8
8
|
} from '../errors/index.js';
|
|
9
9
|
|
|
10
|
-
const DEFAULT_TOOL_TIMEOUT = 120000;
|
|
11
|
-
const DEFAULT_LLM_TIMEOUT = 120000;
|
|
10
|
+
const DEFAULT_TOOL_TIMEOUT = process.env.AGENT_DEFAULT_TOOL_TIMEOUT || 120000;
|
|
11
|
+
const DEFAULT_LLM_TIMEOUT = process.env.AGENT_DEFAULT_LLM_TIMEOUT || 120000;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Emits an event to the hooks system if hooks are available
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
memoryStore,
|
|
10
10
|
session
|
|
11
11
|
} from "./store/store.js";
|
|
12
|
+
import { Conversation } from "./utils/conversation.js";
|
|
12
13
|
|
|
13
14
|
export const AgentLoaderFile = _AgentLoader.AgentLoaderFile
|
|
14
15
|
export const AgentLoaderJSON = _AgentLoader.AgentLoaderJSON
|
|
@@ -112,4 +113,6 @@ export class Response {
|
|
|
112
113
|
this.#content = parsed.content
|
|
113
114
|
this.#session = parsed.session
|
|
114
115
|
}
|
|
115
|
-
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { Conversation };
|
package/src/llm/base.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { logger } from '../utils/logger.js'
|
|
2
2
|
import { LLMError } from '../errors/index.js'
|
|
3
|
+
import { Conversation } from '../utils/conversation.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Base class for LLM implementations
|
|
@@ -47,7 +48,7 @@ export class BaseLLM {
|
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Add a user prompt to the conversation
|
|
50
|
-
* @param {Array} conversation - The conversation history
|
|
51
|
+
* @param {Array|Conversation} conversation - The conversation history
|
|
51
52
|
* @param {string} formattedPrompt - The formatted user prompt
|
|
52
53
|
* @returns {Promise<void>}
|
|
53
54
|
*/
|
|
@@ -56,6 +57,16 @@ export class BaseLLM {
|
|
|
56
57
|
promptPreview: formattedPrompt.substring(0, 100)
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
// Handle both raw array and Conversation object
|
|
61
|
+
if (conversation instanceof Conversation) {
|
|
62
|
+
// Subclasses should override this to add their specific format
|
|
63
|
+
// But we'll add a generic format by default
|
|
64
|
+
conversation.addUserMessage({
|
|
65
|
+
role: 'user',
|
|
66
|
+
content: formattedPrompt
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// For array, we rely on subclass implementations
|
|
59
70
|
// Subclasses must implement appropriate conversation format
|
|
60
71
|
}
|
|
61
72
|
|
|
@@ -83,10 +94,7 @@ export class BaseLLM {
|
|
|
83
94
|
* @returns {Promise<any>} Result of the tool execution
|
|
84
95
|
*/
|
|
85
96
|
async executeToolCall(toolCall, name, args, state, toolsAndHandoffsMap) {
|
|
86
|
-
logger.info(`Executing tool from ${this.type}
|
|
87
|
-
toolName: name,
|
|
88
|
-
argsPreview: JSON.stringify(args).substring(0, 100)
|
|
89
|
-
});
|
|
97
|
+
logger.info(`Executing tool from ${this.type} - Tool: ${name} - Args: ${JSON.stringify(args).substring(0, 100)}`);
|
|
90
98
|
|
|
91
99
|
try {
|
|
92
100
|
if (!toolsAndHandoffsMap[name] || !toolsAndHandoffsMap[name].function) {
|
|
@@ -104,7 +112,6 @@ export class BaseLLM {
|
|
|
104
112
|
|
|
105
113
|
logger.debug('Tool execution successful', { toolName: name });
|
|
106
114
|
return result;
|
|
107
|
-
|
|
108
115
|
} catch (error) {
|
|
109
116
|
logger.error(`Error executing tool "${name}"`, error.message);
|
|
110
117
|
throw error;
|
package/src/llm/gemini.js
CHANGED
|
@@ -2,6 +2,7 @@ import { GoogleGenAI } from '@google/genai'
|
|
|
2
2
|
import { logger } from '../utils/logger.js'
|
|
3
3
|
import { LLMError } from '../errors/index.js'
|
|
4
4
|
import { BaseLLM } from './base.js'
|
|
5
|
+
import { Conversation } from '../utils/conversation.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Gemini LLM implementation
|
|
@@ -41,7 +42,13 @@ class GeminiLLM extends BaseLLM {
|
|
|
41
42
|
*/
|
|
42
43
|
async callModel(llmClientConfig, context) {
|
|
43
44
|
const { client, toolsAndHandoffsMap, conversation } = context;
|
|
44
|
-
|
|
45
|
+
|
|
46
|
+
// Get raw conversation if it's a Conversation object
|
|
47
|
+
const conversationArray = conversation instanceof Conversation
|
|
48
|
+
? conversation.getRawConversation()
|
|
49
|
+
: conversation;
|
|
50
|
+
|
|
51
|
+
const input = { ...llmClientConfig, contents: conversationArray };
|
|
45
52
|
|
|
46
53
|
// Configure tools if provided
|
|
47
54
|
if (input.config !== undefined && input.tools !== undefined) {
|
|
@@ -53,7 +60,7 @@ class GeminiLLM extends BaseLLM {
|
|
|
53
60
|
|
|
54
61
|
logger.debug('Calling Gemini model', {
|
|
55
62
|
model: input.model,
|
|
56
|
-
conversationLength:
|
|
63
|
+
conversationLength: conversationArray.length,
|
|
57
64
|
toolsCount: toolsAndHandoffsMap.tools.length
|
|
58
65
|
});
|
|
59
66
|
|
|
@@ -86,7 +93,7 @@ class GeminiLLM extends BaseLLM {
|
|
|
86
93
|
* Handle a specific tool call from Gemini response
|
|
87
94
|
* @param {Object} toolCall - The tool call to process
|
|
88
95
|
* @param {Object} state - Current application state
|
|
89
|
-
* @param {Array} conversation - The conversation history
|
|
96
|
+
* @param {Array|Conversation} conversation - The conversation history
|
|
90
97
|
* @param {Object} toolsAndHandoffsMap - Map of available tools
|
|
91
98
|
*/
|
|
92
99
|
async handleToolCall(toolCall, state, conversation, toolsAndHandoffsMap) {
|
|
@@ -102,8 +109,16 @@ class GeminiLLM extends BaseLLM {
|
|
|
102
109
|
response: typeof result === 'string' ? { answer: result } : result
|
|
103
110
|
};
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
const functionCallMessage = { role: 'model', parts: [{ functionCall: toolCall }] };
|
|
113
|
+
const functionResponseMessage = { role: 'user', parts: [{ functionResponse: function_response_part }] };
|
|
114
|
+
|
|
115
|
+
if (conversation instanceof Conversation) {
|
|
116
|
+
conversation.addFunctionCall(functionCallMessage);
|
|
117
|
+
conversation.addFunctionResult(functionResponseMessage);
|
|
118
|
+
} else {
|
|
119
|
+
conversation.push(functionCallMessage);
|
|
120
|
+
conversation.push(functionResponseMessage);
|
|
121
|
+
}
|
|
107
122
|
|
|
108
123
|
} catch (error) {
|
|
109
124
|
// Return error as function response in Gemini-specific format
|
|
@@ -112,15 +127,23 @@ class GeminiLLM extends BaseLLM {
|
|
|
112
127
|
response: { error: error.message }
|
|
113
128
|
};
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
const functionCallMessage = { role: 'model', parts: [{ functionCall: toolCall }] };
|
|
131
|
+
const functionResponseMessage = { role: 'user', parts: [{ functionResponse: errorResponse }] };
|
|
132
|
+
|
|
133
|
+
if (conversation instanceof Conversation) {
|
|
134
|
+
conversation.addFunctionCall(functionCallMessage);
|
|
135
|
+
conversation.addFunctionResult(functionResponseMessage);
|
|
136
|
+
} else {
|
|
137
|
+
conversation.push(functionCallMessage);
|
|
138
|
+
conversation.push(functionResponseMessage);
|
|
139
|
+
}
|
|
117
140
|
}
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
/**
|
|
121
144
|
* Processes the model response, handling text responses and function calls
|
|
122
145
|
* @param {Object} state - Current application state
|
|
123
|
-
* @param {Array} conversation - The conversation history
|
|
146
|
+
* @param {Array|Conversation} conversation - The conversation history
|
|
124
147
|
* @param {Object} toolsAndHandoffsMap - Map of available tools
|
|
125
148
|
* @param {Object} response - The model response to process
|
|
126
149
|
* @returns {Promise<string|null>} Text response or null if processing tool calls
|
|
@@ -129,6 +152,15 @@ class GeminiLLM extends BaseLLM {
|
|
|
129
152
|
// Handle simple text response
|
|
130
153
|
if (response.text !== undefined) {
|
|
131
154
|
logger.debug('Gemini response contains text, returning directly');
|
|
155
|
+
|
|
156
|
+
if (conversation instanceof Conversation) {
|
|
157
|
+
const modelResponse = {
|
|
158
|
+
role: 'model',
|
|
159
|
+
parts: [{ text: response.text }]
|
|
160
|
+
};
|
|
161
|
+
conversation.addModelResponse(modelResponse);
|
|
162
|
+
}
|
|
163
|
+
|
|
132
164
|
return response.text;
|
|
133
165
|
}
|
|
134
166
|
|
|
@@ -149,17 +181,23 @@ class GeminiLLM extends BaseLLM {
|
|
|
149
181
|
|
|
150
182
|
/**
|
|
151
183
|
* Adds a user prompt to the conversation
|
|
152
|
-
* @param {Array} conversation - The conversation history
|
|
184
|
+
* @param {Array|Conversation} conversation - The conversation history
|
|
153
185
|
* @param {string} formattedPrompt - The formatted user prompt
|
|
154
186
|
* @returns {Promise<void>}
|
|
155
187
|
*/
|
|
156
188
|
async prompt(conversation, formattedPrompt) {
|
|
157
189
|
await super.prompt(conversation, formattedPrompt);
|
|
158
190
|
|
|
159
|
-
|
|
191
|
+
const userMessage = {
|
|
160
192
|
role: 'user',
|
|
161
193
|
parts: [{ text: formattedPrompt }]
|
|
162
|
-
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (conversation instanceof Conversation) {
|
|
197
|
+
conversation.addUserMessage(userMessage);
|
|
198
|
+
} else {
|
|
199
|
+
conversation.push(userMessage);
|
|
200
|
+
}
|
|
163
201
|
}
|
|
164
202
|
}
|
|
165
203
|
|
package/src/llm/gpt.js
CHANGED
|
@@ -2,6 +2,7 @@ import OpenAI from 'openai'
|
|
|
2
2
|
import { logger } from '../utils/logger.js'
|
|
3
3
|
import { LLMError } from '../errors/index.js'
|
|
4
4
|
import { BaseLLM } from './base.js'
|
|
5
|
+
import { Conversation } from '../utils/conversation.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* OpenAI LLM implementation
|
|
@@ -43,11 +44,15 @@ class OpenAILLM extends BaseLLM {
|
|
|
43
44
|
const { client, toolsAndHandoffsMap, conversation } = context;
|
|
44
45
|
const input = { ...llmClientConfig };
|
|
45
46
|
input.tools = toolsAndHandoffsMap.tools;
|
|
46
|
-
|
|
47
|
+
|
|
48
|
+
// Get raw conversation if it's a Conversation object
|
|
49
|
+
input.input = conversation instanceof Conversation
|
|
50
|
+
? conversation.getRawConversation()
|
|
51
|
+
: conversation;
|
|
47
52
|
|
|
48
53
|
logger.debug('Calling OpenAI model', {
|
|
49
54
|
model: input.model,
|
|
50
|
-
conversationLength:
|
|
55
|
+
conversationLength: input.input.length,
|
|
51
56
|
toolsCount: toolsAndHandoffsMap.tools.length
|
|
52
57
|
});
|
|
53
58
|
//console.log(JSON.stringify(input, null, 2))
|
|
@@ -76,7 +81,7 @@ class OpenAILLM extends BaseLLM {
|
|
|
76
81
|
* Handle a specific tool call from OpenAI response
|
|
77
82
|
* @param {Object} toolCall - The tool call to process
|
|
78
83
|
* @param {Object} state - Current application state
|
|
79
|
-
* @param {Array} conversation - The conversation history
|
|
84
|
+
* @param {Array|Conversation} conversation - The conversation history
|
|
80
85
|
* @param {Object} toolsAndHandoffsMap - Map of available tools
|
|
81
86
|
*/
|
|
82
87
|
async handleToolCall(toolCall, state, conversation, toolsAndHandoffsMap) {
|
|
@@ -91,7 +96,13 @@ class OpenAILLM extends BaseLLM {
|
|
|
91
96
|
});
|
|
92
97
|
|
|
93
98
|
const result = await super.executeToolCall(toolCall, name, args, state, toolsAndHandoffsMap);
|
|
94
|
-
|
|
99
|
+
|
|
100
|
+
// Add function call to conversation
|
|
101
|
+
if (conversation instanceof Conversation) {
|
|
102
|
+
conversation.addFunctionCall(toolCall);
|
|
103
|
+
} else {
|
|
104
|
+
conversation.push(toolCall);
|
|
105
|
+
}
|
|
95
106
|
|
|
96
107
|
const resultString = typeof result === 'string' ? result : JSON.stringify(result);
|
|
97
108
|
|
|
@@ -100,28 +111,47 @@ class OpenAILLM extends BaseLLM {
|
|
|
100
111
|
resultPreview: resultString.substring(0, 100)
|
|
101
112
|
});
|
|
102
113
|
|
|
103
|
-
|
|
114
|
+
const functionOutput = {
|
|
104
115
|
type: "function_call_output",
|
|
105
116
|
call_id: toolCall.call_id,
|
|
106
117
|
output: resultString
|
|
107
|
-
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Add function result to conversation
|
|
121
|
+
if (conversation instanceof Conversation) {
|
|
122
|
+
conversation.addFunctionResult(functionOutput);
|
|
123
|
+
} else {
|
|
124
|
+
conversation.push(functionOutput);
|
|
125
|
+
}
|
|
108
126
|
} catch (error) {
|
|
109
127
|
logger.error(`Error executing tool "${toolCall.name}"`, { error });
|
|
110
128
|
|
|
111
129
|
// Add error as function output
|
|
112
|
-
conversation
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
if (conversation instanceof Conversation) {
|
|
131
|
+
conversation.addFunctionCall(toolCall);
|
|
132
|
+
|
|
133
|
+
const errorOutput = {
|
|
134
|
+
type: "function_call_output",
|
|
135
|
+
call_id: toolCall.call_id,
|
|
136
|
+
output: JSON.stringify({ error: error.message })
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
conversation.addFunctionResult(errorOutput);
|
|
140
|
+
} else {
|
|
141
|
+
conversation.push(toolCall);
|
|
142
|
+
conversation.push({
|
|
143
|
+
type: "function_call_output",
|
|
144
|
+
call_id: toolCall.call_id,
|
|
145
|
+
output: JSON.stringify({ error: error.message })
|
|
146
|
+
});
|
|
147
|
+
}
|
|
118
148
|
}
|
|
119
149
|
}
|
|
120
150
|
|
|
121
151
|
/**
|
|
122
152
|
* Processes the model response, handling text responses and function calls
|
|
123
153
|
* @param {Object} state - Current application state
|
|
124
|
-
* @param {Array} conversation - The conversation history
|
|
154
|
+
* @param {Array|Conversation} conversation - The conversation history
|
|
125
155
|
* @param {Object} toolsAndHandoffsMap - Map of available tools
|
|
126
156
|
* @param {Object} response - The model response to process
|
|
127
157
|
* @returns {Promise<string|null>} Text response or null if processing tool calls
|
|
@@ -129,6 +159,15 @@ class OpenAILLM extends BaseLLM {
|
|
|
129
159
|
async onResponse(state, conversation, toolsAndHandoffsMap, response) {
|
|
130
160
|
if (response.output_text !== undefined && response.output_text.length > 0) {
|
|
131
161
|
logger.debug('OpenAI response contains text, returning directly');
|
|
162
|
+
|
|
163
|
+
// Add model response to conversation if using Conversation object
|
|
164
|
+
if (conversation instanceof Conversation) {
|
|
165
|
+
conversation.addModelResponse({
|
|
166
|
+
role: 'assistant',
|
|
167
|
+
content: response.output_text
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
132
171
|
return response.output_text;
|
|
133
172
|
}
|
|
134
173
|
|
|
@@ -142,7 +181,11 @@ class OpenAILLM extends BaseLLM {
|
|
|
142
181
|
|
|
143
182
|
// Add reasoning to conversation
|
|
144
183
|
for (const res of reasoning) {
|
|
145
|
-
conversation
|
|
184
|
+
if (conversation instanceof Conversation) {
|
|
185
|
+
conversation.addModelResponse(res);
|
|
186
|
+
} else {
|
|
187
|
+
conversation.push(res);
|
|
188
|
+
}
|
|
146
189
|
}
|
|
147
190
|
|
|
148
191
|
// Process all tool calls sequentially
|
|
@@ -155,17 +198,23 @@ class OpenAILLM extends BaseLLM {
|
|
|
155
198
|
|
|
156
199
|
/**
|
|
157
200
|
* Adds a user prompt to the conversation
|
|
158
|
-
* @param {Array} conversation - The conversation history
|
|
201
|
+
* @param {Array|Conversation} conversation - The conversation history
|
|
159
202
|
* @param {string} formattedPrompt - The formatted user prompt
|
|
160
203
|
* @returns {Promise<void>}
|
|
161
204
|
*/
|
|
162
205
|
async prompt(conversation, formattedPrompt) {
|
|
163
206
|
await super.prompt(conversation, formattedPrompt);
|
|
164
207
|
|
|
165
|
-
|
|
208
|
+
const userMessage = {
|
|
166
209
|
role: 'user',
|
|
167
210
|
content: formattedPrompt
|
|
168
|
-
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (conversation instanceof Conversation) {
|
|
214
|
+
conversation.addUserMessage(userMessage);
|
|
215
|
+
} else {
|
|
216
|
+
conversation.push(userMessage);
|
|
217
|
+
}
|
|
169
218
|
}
|
|
170
219
|
}
|
|
171
220
|
|
package/src/store/store.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { v4 as uuid } from 'uuid'
|
|
2
2
|
import { createClient } from 'redis'
|
|
3
3
|
import pgp from 'pg-promise'
|
|
4
|
+
import { Conversation } from '../utils/conversation.js'
|
|
4
5
|
|
|
5
6
|
export function session (id) {
|
|
6
7
|
let state = {}
|
|
7
|
-
let
|
|
8
|
+
let conversationManager = new Conversation()
|
|
8
9
|
|
|
9
10
|
return {
|
|
10
11
|
query: async function (agentInstance, input) {
|
|
11
|
-
return await agentInstance.query(state,
|
|
12
|
+
return await agentInstance.query(state, conversationManager.getRawConversation(), input)
|
|
12
13
|
},
|
|
13
14
|
setState: function (_state) {
|
|
14
15
|
state = _state
|
|
@@ -22,33 +23,39 @@ export function session (id) {
|
|
|
22
23
|
return state
|
|
23
24
|
},
|
|
24
25
|
trimConversation: function (elementsToKeep) {
|
|
25
|
-
|
|
26
|
-
let additionalElementsToRemove = 0
|
|
27
|
-
for (const chatIndex in conversation) {
|
|
28
|
-
if (conversation[chatIndex].role !== 'user' || conversation[chatIndex].role == undefined) {
|
|
29
|
-
additionalElementsToRemove += 1
|
|
30
|
-
} else {
|
|
31
|
-
break
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (additionalElementsToRemove > 0) {
|
|
35
|
-
conversation = conversation.slice(additionalElementsToRemove)
|
|
36
|
-
}
|
|
26
|
+
conversationManager.trim(elementsToKeep)
|
|
37
27
|
},
|
|
38
28
|
setConversation: function (_conversation) {
|
|
39
|
-
|
|
29
|
+
// Support both array and Conversation objects
|
|
30
|
+
if (_conversation instanceof Conversation) {
|
|
31
|
+
conversationManager = _conversation;
|
|
32
|
+
} else if (Array.isArray(_conversation)) {
|
|
33
|
+
conversationManager.importFromArray(_conversation);
|
|
34
|
+
}
|
|
40
35
|
},
|
|
41
36
|
getConversation: function () {
|
|
42
|
-
|
|
37
|
+
// Return raw conversation for backward compatibility
|
|
38
|
+
return conversationManager.getRawConversation()
|
|
39
|
+
},
|
|
40
|
+
getConversationManager: function() {
|
|
41
|
+
return conversationManager
|
|
43
42
|
},
|
|
44
43
|
load: async function (stateStore) {
|
|
45
44
|
const _state = await stateStore.get(id)
|
|
46
45
|
if (_state !== null) {
|
|
47
46
|
const parsedState = JSON.parse(_state)
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
// Handle both old-style conversation array and new-style serialized Conversation
|
|
49
|
+
if (parsedState.conversationData) {
|
|
50
|
+
conversationManager.deserialize(parsedState.conversationData)
|
|
51
|
+
} else if (Array.isArray(parsedState.conversation)) {
|
|
52
|
+
conversationManager.importFromArray(parsedState.conversation)
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
state = parsedState.state || {}
|
|
50
56
|
return {
|
|
51
|
-
conversation
|
|
57
|
+
// Return raw conversation for backward compatibility
|
|
58
|
+
conversation: conversationManager.getRawConversation(),
|
|
52
59
|
state: state
|
|
53
60
|
}
|
|
54
61
|
}
|
|
@@ -59,8 +66,11 @@ export function session (id) {
|
|
|
59
66
|
},
|
|
60
67
|
dump: async function (stateStore) {
|
|
61
68
|
return await stateStore.set(id, JSON.stringify({
|
|
62
|
-
conversation
|
|
63
|
-
|
|
69
|
+
// Keep conversation array for backward compatibility
|
|
70
|
+
conversation: conversationManager.getRawConversation(),
|
|
71
|
+
// Add serialized conversation data with metadata
|
|
72
|
+
conversationData: conversationManager.serialize(),
|
|
73
|
+
state: state
|
|
64
74
|
}))
|
|
65
75
|
},
|
|
66
76
|
}
|
package/src/transport/nats.js
CHANGED
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
} from '../errors/index.js';
|
|
14
14
|
|
|
15
15
|
// Constants
|
|
16
|
-
const HEARTBEAT_INTERVAL = 1000;
|
|
17
|
-
const TIMEOUT_TASK_REQUEST = 120000;
|
|
16
|
+
const HEARTBEAT_INTERVAL = process.env.AGENTNET_NATS_HEARTBEAT_INTERVAL || 1000;
|
|
17
|
+
const TIMEOUT_TASK_REQUEST = process.env.AGENTNET_NATS_TIMEOUT_TASK_REQUEST || 120000;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* NATS implementation of the Transport interface
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Class representing a conversation with additional metadata for each message
|
|
5
|
+
*/
|
|
6
|
+
export class Conversation {
|
|
7
|
+
/**
|
|
8
|
+
* Create a new Conversation instance
|
|
9
|
+
*/
|
|
10
|
+
constructor() {
|
|
11
|
+
// The internal conversation array with metadata
|
|
12
|
+
this.messages = [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a message to the conversation
|
|
17
|
+
* @param {Object} message - The message to add
|
|
18
|
+
* @param {Object} metadata - Additional metadata
|
|
19
|
+
* @param {string} metadata.type - Message type ('user_input', 'model_response', 'function_call', 'function_result')
|
|
20
|
+
*/
|
|
21
|
+
addMessage(message, metadata = {}) {
|
|
22
|
+
this.messages.push({
|
|
23
|
+
content: message,
|
|
24
|
+
metadata: {
|
|
25
|
+
...metadata,
|
|
26
|
+
timestamp: new Date().toISOString()
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add a user message to the conversation
|
|
33
|
+
* @param {Object} message - The user message
|
|
34
|
+
*/
|
|
35
|
+
addUserMessage(message) {
|
|
36
|
+
this.addMessage(message, { type: 'user_input' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add a model response to the conversation
|
|
41
|
+
* @param {Object} message - The model response
|
|
42
|
+
*/
|
|
43
|
+
addModelResponse(message) {
|
|
44
|
+
this.addMessage(message, { type: 'model_response' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add a function call to the conversation
|
|
49
|
+
* @param {Object} message - The function call
|
|
50
|
+
*/
|
|
51
|
+
addFunctionCall(message) {
|
|
52
|
+
this.addMessage(message, { type: 'function_call' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add a function result to the conversation
|
|
57
|
+
* @param {Object} message - The function result
|
|
58
|
+
*/
|
|
59
|
+
addFunctionResult(message) {
|
|
60
|
+
this.addMessage(message, { type: 'function_result' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the conversation messages
|
|
65
|
+
* @returns {Array} The messages in the conversation
|
|
66
|
+
*/
|
|
67
|
+
getMessages() {
|
|
68
|
+
return this.messages;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the raw conversation history compatible with LLM providers
|
|
73
|
+
* This strips out the metadata and returns just the message content
|
|
74
|
+
* @returns {Array} The raw conversation history
|
|
75
|
+
*/
|
|
76
|
+
getRawConversation() {
|
|
77
|
+
return this.messages.map(message => message.content);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set the conversation from an existing array
|
|
82
|
+
* @param {Array} conversation - Existing conversation array
|
|
83
|
+
* @param {Object} options - Import options
|
|
84
|
+
* @param {boolean} options.detectTypes - Try to automatically detect message types
|
|
85
|
+
*/
|
|
86
|
+
importFromArray(conversation, options = { detectTypes: true }) {
|
|
87
|
+
this.messages = [];
|
|
88
|
+
|
|
89
|
+
if (!Array.isArray(conversation)) {
|
|
90
|
+
logger.warn('Attempted to import non-array conversation');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const message of conversation) {
|
|
95
|
+
let type = 'unknown';
|
|
96
|
+
|
|
97
|
+
// Try to automatically detect message types if enabled
|
|
98
|
+
if (options.detectTypes) {
|
|
99
|
+
if (message.role === 'user') {
|
|
100
|
+
// Check if it's a function response (from function result)
|
|
101
|
+
if (message.parts && message.parts[0] && message.parts[0].functionResponse) {
|
|
102
|
+
type = 'function_result';
|
|
103
|
+
} else {
|
|
104
|
+
type = 'user_input';
|
|
105
|
+
}
|
|
106
|
+
} else if (message.role === 'model' || message.role === 'assistant') {
|
|
107
|
+
// Check if it's a function call
|
|
108
|
+
if ((message.parts && message.parts[0] && message.parts[0].functionCall) ||
|
|
109
|
+
message.type === 'function_call') {
|
|
110
|
+
type = 'function_call';
|
|
111
|
+
} else {
|
|
112
|
+
type = 'model_response';
|
|
113
|
+
}
|
|
114
|
+
} else if (message.type === 'function_call') {
|
|
115
|
+
type = 'function_call';
|
|
116
|
+
} else if (message.type === 'function_call_output') {
|
|
117
|
+
type = 'function_result';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.addMessage(message, { type });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Trim the conversation to a maximum number of elements
|
|
127
|
+
* This will keep at least one user message at the start
|
|
128
|
+
* @param {number} maxElements - Maximum number of elements to keep
|
|
129
|
+
*/
|
|
130
|
+
trim(maxElements) {
|
|
131
|
+
if (this.messages.length <= maxElements) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// First, keep only the latest maxElements
|
|
136
|
+
this.messages = this.messages.slice(-maxElements);
|
|
137
|
+
|
|
138
|
+
// Then, ensure we have a user message at the start
|
|
139
|
+
// Find the first user message
|
|
140
|
+
let firstUserMessageIndex = -1;
|
|
141
|
+
for (let i = 0; i < this.messages.length; i++) {
|
|
142
|
+
if (this.messages[i].metadata.type === 'user_input') {
|
|
143
|
+
firstUserMessageIndex = i;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If we didn't find a user message, or it's already at the start, nothing to do
|
|
149
|
+
if (firstUserMessageIndex === -1 || firstUserMessageIndex === 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Otherwise, trim all messages before the first user message
|
|
154
|
+
this.messages = this.messages.slice(firstUserMessageIndex);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Serialize the conversation to an object
|
|
159
|
+
* @returns {Object} The serialized conversation
|
|
160
|
+
*/
|
|
161
|
+
serialize() {
|
|
162
|
+
return {
|
|
163
|
+
messages: this.messages
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Deserialize the conversation from an object
|
|
169
|
+
* @param {Object} data - The serialized conversation
|
|
170
|
+
* @returns {Conversation} The deserialized conversation
|
|
171
|
+
*/
|
|
172
|
+
deserialize(data) {
|
|
173
|
+
if (data && data.messages) {
|
|
174
|
+
this.messages = data.messages;
|
|
175
|
+
}
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
}
|