@toolplex/ai-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +292 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +9 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +137 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +14 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/core/ChatEngine.d.ts +47 -0
- package/dist/core/ChatEngine.d.ts.map +1 -0
- package/dist/core/ChatEngine.js +355 -0
- package/dist/core/ChatEngine.js.map +1 -0
- package/dist/core/ToolBuilder.d.ts +25 -0
- package/dist/core/ToolBuilder.d.ts.map +1 -0
- package/dist/core/ToolBuilder.js +215 -0
- package/dist/core/ToolBuilder.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/DefaultStdioTransportFactory.d.ts +27 -0
- package/dist/mcp/DefaultStdioTransportFactory.d.ts.map +1 -0
- package/dist/mcp/DefaultStdioTransportFactory.js +60 -0
- package/dist/mcp/DefaultStdioTransportFactory.js.map +1 -0
- package/dist/mcp/MCPClient.d.ts +60 -0
- package/dist/mcp/MCPClient.d.ts.map +1 -0
- package/dist/mcp/MCPClient.js +164 -0
- package/dist/mcp/MCPClient.js.map +1 -0
- package/dist/mcp/index.d.ts +10 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/paths.d.ts +16 -0
- package/dist/mcp/paths.d.ts.map +1 -0
- package/dist/mcp/paths.js +58 -0
- package/dist/mcp/paths.js.map +1 -0
- package/dist/mcp/types.d.ts +85 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/providers/index.d.ts +40 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +148 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/toolplex.d.ts +43 -0
- package/dist/providers/toolplex.d.ts.map +1 -0
- package/dist/providers/toolplex.js +168 -0
- package/dist/providers/toolplex.js.map +1 -0
- package/dist/types/index.d.ts +218 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/models.d.ts +30 -0
- package/dist/utils/models.d.ts.map +1 -0
- package/dist/utils/models.js +52 -0
- package/dist/utils/models.js.map +1 -0
- package/dist/utils/schema.d.ts +74 -0
- package/dist/utils/schema.d.ts.map +1 -0
- package/dist/utils/schema.js +253 -0
- package/dist/utils/schema.js.map +1 -0
- package/package.json +70 -0
- package/src/adapters/index.ts +9 -0
- package/src/adapters/types.ts +241 -0
- package/src/core/ChatEngine.ts +464 -0
- package/src/core/ToolBuilder.ts +323 -0
- package/src/core/index.ts +6 -0
- package/src/index.ts +86 -0
- package/src/mcp/DefaultStdioTransportFactory.ts +71 -0
- package/src/mcp/MCPClient.ts +209 -0
- package/src/mcp/index.ts +24 -0
- package/src/mcp/paths.ts +91 -0
- package/src/mcp/types.ts +93 -0
- package/src/providers/index.ts +177 -0
- package/src/providers/toolplex.ts +217 -0
- package/src/types/index.ts +290 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/models.ts +59 -0
- package/src/utils/schema.ts +307 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - Adapter Interface
|
|
3
|
+
*
|
|
4
|
+
* The EngineAdapter interface defines how the AI engine interacts with
|
|
5
|
+
* the host platform. This abstraction allows the same engine core to
|
|
6
|
+
* run in Electron, server-side (cloud), or CLI environments.
|
|
7
|
+
*
|
|
8
|
+
* Implementations:
|
|
9
|
+
* - ElectronAdapter: Desktop app with IPC, webContents, confirmations
|
|
10
|
+
* - HTTPAdapter: Cloud/server with HTTP streaming, no confirmations
|
|
11
|
+
* - CLIAdapter: CLI with terminal I/O
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ProviderCredentials,
|
|
16
|
+
ConfirmationRequest,
|
|
17
|
+
ConfirmationResult,
|
|
18
|
+
MCPResult,
|
|
19
|
+
MCPSessionInfo,
|
|
20
|
+
MCPTool,
|
|
21
|
+
MCPToolResult,
|
|
22
|
+
UsageData,
|
|
23
|
+
ChatSession,
|
|
24
|
+
ChatMessage,
|
|
25
|
+
} from "../types/index.js";
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Event Emitter Interface
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Interface for emitting engine events to the platform
|
|
33
|
+
*/
|
|
34
|
+
export interface EngineEventEmitter {
|
|
35
|
+
/** Emit a text chunk during streaming */
|
|
36
|
+
emitChunk(streamId: string, chunk: string): void;
|
|
37
|
+
|
|
38
|
+
/** Emit stream completion */
|
|
39
|
+
emitComplete(streamId: string, fullText: string, usage?: UsageData): void;
|
|
40
|
+
|
|
41
|
+
/** Emit stream error */
|
|
42
|
+
emitError(streamId: string, error: string): void;
|
|
43
|
+
|
|
44
|
+
/** Emit tool input start (for streaming tool arguments) */
|
|
45
|
+
emitToolInputStart(
|
|
46
|
+
streamId: string,
|
|
47
|
+
toolCallId: string,
|
|
48
|
+
toolName: string,
|
|
49
|
+
): void;
|
|
50
|
+
|
|
51
|
+
/** Emit tool input delta (streaming argument chunks) */
|
|
52
|
+
emitToolInputDelta(
|
|
53
|
+
streamId: string,
|
|
54
|
+
toolCallId: string,
|
|
55
|
+
argsDelta: string,
|
|
56
|
+
): void;
|
|
57
|
+
|
|
58
|
+
/** Emit tool execution result */
|
|
59
|
+
emitToolResult(
|
|
60
|
+
streamId: string,
|
|
61
|
+
toolCallId: string,
|
|
62
|
+
result: MCPToolResult,
|
|
63
|
+
toolName: string,
|
|
64
|
+
args: any,
|
|
65
|
+
): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Confirmation Handler Interface
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Interface for handling user confirmations
|
|
74
|
+
* Desktop: Shows modal dialogs via IPC
|
|
75
|
+
* Cloud: Auto-approves or uses policy-based decisions
|
|
76
|
+
* CLI: Prompts in terminal
|
|
77
|
+
*/
|
|
78
|
+
export interface ConfirmationHandler {
|
|
79
|
+
/**
|
|
80
|
+
* Request user confirmation for a tool operation
|
|
81
|
+
* @param streamId - The current stream ID
|
|
82
|
+
* @param request - The confirmation request details
|
|
83
|
+
* @returns Confirmation result with allowed/denied and any edits
|
|
84
|
+
*/
|
|
85
|
+
requestConfirmation(
|
|
86
|
+
streamId: string,
|
|
87
|
+
request: ConfirmationRequest,
|
|
88
|
+
): Promise<ConfirmationResult>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether this handler supports interactive confirmations
|
|
92
|
+
* Cloud handlers may return false to auto-approve based on policy
|
|
93
|
+
*/
|
|
94
|
+
isInteractive(): boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// MCP Transport Interface
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Interface for MCP transport operations
|
|
103
|
+
* Abstracts away the transport mechanism (stdio, HTTP, etc.)
|
|
104
|
+
*/
|
|
105
|
+
export interface MCPTransportAdapter {
|
|
106
|
+
/** Create/connect MCP transport for a session */
|
|
107
|
+
createTransport(
|
|
108
|
+
sessionId: string,
|
|
109
|
+
apiKey: string,
|
|
110
|
+
sessionResumeHistory?: string,
|
|
111
|
+
): Promise<MCPResult>;
|
|
112
|
+
|
|
113
|
+
/** Get session info */
|
|
114
|
+
getSessionInfo(sessionId: string): MCPSessionInfo;
|
|
115
|
+
|
|
116
|
+
/** List available tools from MCP server */
|
|
117
|
+
listTools(sessionId: string): Promise<{ tools: MCPTool[] }>;
|
|
118
|
+
|
|
119
|
+
/** Call an MCP tool */
|
|
120
|
+
callTool(
|
|
121
|
+
sessionId: string,
|
|
122
|
+
toolName: string,
|
|
123
|
+
args: any,
|
|
124
|
+
): Promise<MCPToolResult>;
|
|
125
|
+
|
|
126
|
+
/** Destroy/disconnect MCP transport */
|
|
127
|
+
destroyTransport(sessionId: string): Promise<MCPResult>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Credentials Provider Interface
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Interface for accessing API credentials
|
|
136
|
+
* Different platforms load credentials differently
|
|
137
|
+
*/
|
|
138
|
+
export interface CredentialsProvider {
|
|
139
|
+
/** Get provider credentials for AI SDK */
|
|
140
|
+
getCredentials(): Promise<ProviderCredentials>;
|
|
141
|
+
|
|
142
|
+
/** Get ToolPlex API key specifically */
|
|
143
|
+
getToolPlexApiKey(): Promise<string>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============================================================================
|
|
147
|
+
// Persistence Interface (Optional)
|
|
148
|
+
// ============================================================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Interface for persisting chat sessions and messages
|
|
152
|
+
* Optional - cloud may use different storage than desktop
|
|
153
|
+
*/
|
|
154
|
+
export interface PersistenceAdapter {
|
|
155
|
+
// Session operations
|
|
156
|
+
createSession(metadata?: Record<string, any>): Promise<ChatSession>;
|
|
157
|
+
getSession(sessionId: string): Promise<ChatSession | null>;
|
|
158
|
+
updateSession(
|
|
159
|
+
sessionId: string,
|
|
160
|
+
updates: Partial<ChatSession>,
|
|
161
|
+
): Promise<void>;
|
|
162
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
163
|
+
|
|
164
|
+
// Message operations
|
|
165
|
+
saveMessage(
|
|
166
|
+
message: Omit<ChatMessage, "id" | "createdAt">,
|
|
167
|
+
): Promise<ChatMessage>;
|
|
168
|
+
getMessages(sessionId: string): Promise<ChatMessage[]>;
|
|
169
|
+
updateMessage(
|
|
170
|
+
messageId: string,
|
|
171
|
+
updates: Partial<ChatMessage>,
|
|
172
|
+
): Promise<void>;
|
|
173
|
+
deleteMessage(messageId: string): Promise<void>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Logger Interface
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Interface for logging
|
|
182
|
+
* Allows different logging implementations per platform
|
|
183
|
+
*/
|
|
184
|
+
export interface LoggerAdapter {
|
|
185
|
+
debug(message: string, meta?: Record<string, any>): void;
|
|
186
|
+
info(message: string, meta?: Record<string, any>): void;
|
|
187
|
+
warn(message: string, meta?: Record<string, any>): void;
|
|
188
|
+
error(message: string, meta?: Record<string, any>): void;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Main Engine Adapter Interface
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The main adapter interface that platforms must implement
|
|
197
|
+
* This brings together all the sub-interfaces needed by the engine
|
|
198
|
+
*/
|
|
199
|
+
export interface EngineAdapter {
|
|
200
|
+
/** Event emitter for streaming events */
|
|
201
|
+
readonly events: EngineEventEmitter;
|
|
202
|
+
|
|
203
|
+
/** Confirmation handler for user approvals */
|
|
204
|
+
readonly confirmations: ConfirmationHandler;
|
|
205
|
+
|
|
206
|
+
/** MCP transport adapter */
|
|
207
|
+
readonly mcp: MCPTransportAdapter;
|
|
208
|
+
|
|
209
|
+
/** Credentials provider */
|
|
210
|
+
readonly credentials: CredentialsProvider;
|
|
211
|
+
|
|
212
|
+
/** Logger */
|
|
213
|
+
readonly logger: LoggerAdapter;
|
|
214
|
+
|
|
215
|
+
/** Persistence adapter (optional) */
|
|
216
|
+
readonly persistence?: PersistenceAdapter;
|
|
217
|
+
|
|
218
|
+
/** Client version string (for API headers) */
|
|
219
|
+
getClientVersion(): string;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Initialize the adapter
|
|
223
|
+
* Called once when engine is created
|
|
224
|
+
*/
|
|
225
|
+
initialize(): Promise<void>;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Cleanup/shutdown the adapter
|
|
229
|
+
* Called when engine is destroyed
|
|
230
|
+
*/
|
|
231
|
+
shutdown(): Promise<void>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Adapter Factory Type
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Factory function type for creating adapters
|
|
240
|
+
*/
|
|
241
|
+
export type AdapterFactory<TConfig = any> = (config: TConfig) => EngineAdapter;
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - Chat Engine
|
|
3
|
+
*
|
|
4
|
+
* Core streaming engine that orchestrates AI chat sessions.
|
|
5
|
+
* Uses adapters for all platform-specific I/O operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { streamText, stepCountIs } from "ai";
|
|
9
|
+
import type { CoreMessage } from "ai";
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
|
|
12
|
+
import type { EngineAdapter } from "../adapters/types.js";
|
|
13
|
+
import type {
|
|
14
|
+
StreamOptions,
|
|
15
|
+
StreamResult,
|
|
16
|
+
EngineConfig,
|
|
17
|
+
FileAttachment,
|
|
18
|
+
ModelConfigFlags,
|
|
19
|
+
} from "../types/index.js";
|
|
20
|
+
import { getModel, toolplexUsageMap } from "../providers/index.js";
|
|
21
|
+
import { buildMCPTools } from "./ToolBuilder.js";
|
|
22
|
+
|
|
23
|
+
export interface ChatEngineOptions {
|
|
24
|
+
adapter: EngineAdapter;
|
|
25
|
+
config?: EngineConfig;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ChatEngine {
|
|
29
|
+
private adapter: EngineAdapter;
|
|
30
|
+
private config: EngineConfig;
|
|
31
|
+
private initialized: boolean = false;
|
|
32
|
+
|
|
33
|
+
constructor(options: ChatEngineOptions) {
|
|
34
|
+
this.adapter = options.adapter;
|
|
35
|
+
this.config = {
|
|
36
|
+
maxSteps: 50,
|
|
37
|
+
debug: false,
|
|
38
|
+
hiddenTools: ["initialize_toolplex"],
|
|
39
|
+
...options.config,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the engine
|
|
45
|
+
*/
|
|
46
|
+
async initialize(): Promise<void> {
|
|
47
|
+
if (this.initialized) return;
|
|
48
|
+
await this.adapter.initialize();
|
|
49
|
+
this.initialized = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Shutdown the engine
|
|
54
|
+
*/
|
|
55
|
+
async shutdown(): Promise<void> {
|
|
56
|
+
if (!this.initialized) return;
|
|
57
|
+
await this.adapter.shutdown();
|
|
58
|
+
this.initialized = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initialize MCP for a session
|
|
63
|
+
*/
|
|
64
|
+
async initializeMCP(sessionId: string): Promise<void> {
|
|
65
|
+
const apiKey = await this.adapter.credentials.getToolPlexApiKey();
|
|
66
|
+
const sessionInfo = this.adapter.mcp.getSessionInfo(sessionId);
|
|
67
|
+
|
|
68
|
+
if (!sessionInfo.exists) {
|
|
69
|
+
this.adapter.logger.debug("ChatEngine: Initializing MCP transport", {
|
|
70
|
+
sessionId,
|
|
71
|
+
});
|
|
72
|
+
const result = await this.adapter.mcp.createTransport(sessionId, apiKey);
|
|
73
|
+
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
throw new Error(`Failed to create MCP transport: ${result.error}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Initialize a session with ToolPlex context
|
|
82
|
+
*/
|
|
83
|
+
async initializeSession(
|
|
84
|
+
sessionId: string,
|
|
85
|
+
modelId: string,
|
|
86
|
+
provider: string,
|
|
87
|
+
): Promise<{ success: boolean; context?: string; error?: string }> {
|
|
88
|
+
try {
|
|
89
|
+
this.adapter.logger.debug(
|
|
90
|
+
"ChatEngine: Initializing session with ToolPlex",
|
|
91
|
+
{
|
|
92
|
+
sessionId,
|
|
93
|
+
modelId,
|
|
94
|
+
provider,
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Initialize MCP transport
|
|
99
|
+
await this.initializeMCP(sessionId);
|
|
100
|
+
|
|
101
|
+
// Extract model metadata
|
|
102
|
+
const modelParts = modelId.split("/");
|
|
103
|
+
const modelName = modelParts[modelParts.length - 1] || modelId;
|
|
104
|
+
|
|
105
|
+
const toolArgs = {
|
|
106
|
+
llm_context: {
|
|
107
|
+
model_family: provider,
|
|
108
|
+
model_name: modelName,
|
|
109
|
+
model_version: modelId,
|
|
110
|
+
chat_client: "toolplex",
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Call initialize_toolplex to get the context
|
|
115
|
+
const result = await this.adapter.mcp.callTool(
|
|
116
|
+
sessionId,
|
|
117
|
+
"initialize_toolplex",
|
|
118
|
+
toolArgs,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Extract text content from the result
|
|
122
|
+
let contextText = "";
|
|
123
|
+
if (result && typeof result === "object" && result.content) {
|
|
124
|
+
for (const item of result.content) {
|
|
125
|
+
if (item.type === "text" && item.text) {
|
|
126
|
+
contextText += item.text + "\n\n";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} else if (typeof result === "string") {
|
|
130
|
+
contextText = result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
context: contextText.trim(),
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
this.adapter.logger.error("ChatEngine: Failed to initialize session", {
|
|
139
|
+
sessionId,
|
|
140
|
+
error,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Stream a chat completion
|
|
151
|
+
*/
|
|
152
|
+
async stream(options: StreamOptions): Promise<StreamResult> {
|
|
153
|
+
const credentials = await this.adapter.credentials.getCredentials();
|
|
154
|
+
const streamId = options.streamId || randomUUID();
|
|
155
|
+
|
|
156
|
+
const {
|
|
157
|
+
sessionId,
|
|
158
|
+
modelId,
|
|
159
|
+
provider,
|
|
160
|
+
messages,
|
|
161
|
+
tools: providedTools,
|
|
162
|
+
temperature,
|
|
163
|
+
topP,
|
|
164
|
+
fileAttachments,
|
|
165
|
+
modelConfig,
|
|
166
|
+
} = options;
|
|
167
|
+
|
|
168
|
+
this.adapter.logger.debug("ChatEngine: Starting stream", {
|
|
169
|
+
sessionId,
|
|
170
|
+
modelId,
|
|
171
|
+
provider,
|
|
172
|
+
messageCount: messages.length,
|
|
173
|
+
hasTools: !!providedTools,
|
|
174
|
+
hasAttachments: !!fileAttachments?.length,
|
|
175
|
+
streamId,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Create abort controller
|
|
179
|
+
const abortController = new AbortController();
|
|
180
|
+
|
|
181
|
+
// Get the model
|
|
182
|
+
const model = getModel(modelId, credentials, {
|
|
183
|
+
logger: this.adapter.logger,
|
|
184
|
+
clientVersion: this.adapter.getClientVersion(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Build MCP tools
|
|
188
|
+
let mcpTools: Record<string, any> = {};
|
|
189
|
+
const apiKey = await this.adapter.credentials.getToolPlexApiKey();
|
|
190
|
+
if (apiKey) {
|
|
191
|
+
try {
|
|
192
|
+
await this.initializeMCP(sessionId);
|
|
193
|
+
mcpTools = await buildMCPTools({
|
|
194
|
+
sessionId,
|
|
195
|
+
streamId,
|
|
196
|
+
modelId,
|
|
197
|
+
abortSignal: abortController.signal,
|
|
198
|
+
adapter: this.adapter,
|
|
199
|
+
hiddenTools: this.config.hiddenTools,
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.adapter.logger.error(
|
|
203
|
+
"ChatEngine: Failed to initialize MCP tools",
|
|
204
|
+
{
|
|
205
|
+
sessionId,
|
|
206
|
+
error,
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
// Continue without tools
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Merge tools
|
|
214
|
+
const allTools = { ...providedTools, ...mcpTools };
|
|
215
|
+
|
|
216
|
+
// Process messages
|
|
217
|
+
const processedMessages = this.processMessages(
|
|
218
|
+
messages,
|
|
219
|
+
fileAttachments,
|
|
220
|
+
modelConfig,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Track usage data
|
|
224
|
+
let capturedUsage: {
|
|
225
|
+
prompt_tokens: number;
|
|
226
|
+
completion_tokens: number;
|
|
227
|
+
total_tokens: number;
|
|
228
|
+
} | null = null;
|
|
229
|
+
|
|
230
|
+
// Promise for onFinish coordination
|
|
231
|
+
let resolveOnFinish: (() => void) | null = null;
|
|
232
|
+
const onFinishPromise = new Promise<void>((resolve) => {
|
|
233
|
+
resolveOnFinish = resolve;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Prepare stream options
|
|
237
|
+
const streamTextOptions: any = {
|
|
238
|
+
model,
|
|
239
|
+
messages: processedMessages,
|
|
240
|
+
tools: allTools,
|
|
241
|
+
stopWhen: stepCountIs(this.config.maxSteps!),
|
|
242
|
+
temperature,
|
|
243
|
+
topP,
|
|
244
|
+
abortSignal: abortController.signal,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Enforce maxTokens if specified by model config
|
|
248
|
+
if (modelConfig?.enforceMaxTokens && modelConfig?.maxOutputTokens) {
|
|
249
|
+
streamTextOptions.maxTokens = modelConfig.maxOutputTokens;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Start streaming
|
|
253
|
+
const result = streamText({
|
|
254
|
+
...streamTextOptions,
|
|
255
|
+
headers: {
|
|
256
|
+
"x-session-id": sessionId,
|
|
257
|
+
...(provider === "openrouter" && {
|
|
258
|
+
"HTTP-Referer": "https://toolplex.ai",
|
|
259
|
+
"X-Title": "ToolPlex AI",
|
|
260
|
+
}),
|
|
261
|
+
},
|
|
262
|
+
onChunk: async (event: any) => {
|
|
263
|
+
// Capture usage data
|
|
264
|
+
if (provider === "toolplex" && event.chunk) {
|
|
265
|
+
const chunk = event.chunk as any;
|
|
266
|
+
|
|
267
|
+
if (chunk.providerMetadata?.usage || chunk.usage) {
|
|
268
|
+
const usage = chunk.providerMetadata?.usage || chunk.usage;
|
|
269
|
+
const promptTokens = usage.prompt_tokens || usage.input_tokens || 0;
|
|
270
|
+
const completionTokens =
|
|
271
|
+
usage.completion_tokens || usage.output_tokens || 0;
|
|
272
|
+
const totalTokens =
|
|
273
|
+
usage.total_tokens || promptTokens + completionTokens;
|
|
274
|
+
|
|
275
|
+
capturedUsage = {
|
|
276
|
+
prompt_tokens: promptTokens,
|
|
277
|
+
completion_tokens: completionTokens,
|
|
278
|
+
total_tokens: totalTokens,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
onStepFinish: async (event: any) => {
|
|
284
|
+
// Emit step finish event if there are tool calls
|
|
285
|
+
const hasToolCalls = event.toolCalls && event.toolCalls.length > 0;
|
|
286
|
+
|
|
287
|
+
if (hasToolCalls) {
|
|
288
|
+
this.adapter.logger.debug(
|
|
289
|
+
"ChatEngine: Step finished with tool calls",
|
|
290
|
+
{
|
|
291
|
+
sessionId,
|
|
292
|
+
textLength: event.text?.length || 0,
|
|
293
|
+
toolCallCount: event.toolCalls.length,
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
onFinish: async (completion: any) => {
|
|
299
|
+
this.adapter.logger.debug("ChatEngine: Stream finished", {
|
|
300
|
+
sessionId,
|
|
301
|
+
textLength: completion.text?.length,
|
|
302
|
+
finishReason: completion.finishReason,
|
|
303
|
+
usage: completion.usage,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Get usage data
|
|
307
|
+
let usageSource = completion.usage;
|
|
308
|
+
|
|
309
|
+
if (provider === "toolplex") {
|
|
310
|
+
const mapUsage = toolplexUsageMap.get(sessionId);
|
|
311
|
+
if (mapUsage) {
|
|
312
|
+
usageSource = mapUsage;
|
|
313
|
+
toolplexUsageMap.delete(sessionId);
|
|
314
|
+
} else if (capturedUsage) {
|
|
315
|
+
usageSource = capturedUsage;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Emit complete event
|
|
320
|
+
this.adapter.events.emitComplete(
|
|
321
|
+
streamId,
|
|
322
|
+
completion.text || "",
|
|
323
|
+
usageSource
|
|
324
|
+
? {
|
|
325
|
+
promptTokens:
|
|
326
|
+
usageSource.prompt_tokens ||
|
|
327
|
+
usageSource.inputTokens ||
|
|
328
|
+
usageSource.promptTokens ||
|
|
329
|
+
0,
|
|
330
|
+
completionTokens:
|
|
331
|
+
usageSource.completion_tokens ||
|
|
332
|
+
usageSource.outputTokens ||
|
|
333
|
+
usageSource.completionTokens ||
|
|
334
|
+
0,
|
|
335
|
+
totalTokens:
|
|
336
|
+
usageSource.total_tokens || usageSource.totalTokens || 0,
|
|
337
|
+
}
|
|
338
|
+
: undefined,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (resolveOnFinish) {
|
|
342
|
+
resolveOnFinish();
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
onError: (error) => {
|
|
346
|
+
this.adapter.logger.error("ChatEngine: Stream error", {
|
|
347
|
+
error,
|
|
348
|
+
sessionId,
|
|
349
|
+
modelId,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
this.adapter.events.emitError(
|
|
353
|
+
streamId,
|
|
354
|
+
error instanceof Error ? error.message : String(error),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
if (resolveOnFinish) {
|
|
358
|
+
resolveOnFinish();
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
streamId,
|
|
365
|
+
textStream: result.textStream,
|
|
366
|
+
fullStream: result.fullStream,
|
|
367
|
+
onFinishPromise,
|
|
368
|
+
abort: async () => {
|
|
369
|
+
this.adapter.logger.debug("ChatEngine: Aborting stream", {
|
|
370
|
+
streamId,
|
|
371
|
+
sessionId,
|
|
372
|
+
});
|
|
373
|
+
abortController.abort();
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Process messages for streaming (handle attachments, filter empty blocks)
|
|
380
|
+
*/
|
|
381
|
+
private processMessages(
|
|
382
|
+
messages: CoreMessage[],
|
|
383
|
+
fileAttachments?: FileAttachment[],
|
|
384
|
+
modelConfig?: ModelConfigFlags,
|
|
385
|
+
): CoreMessage[] {
|
|
386
|
+
let processedMessages: CoreMessage[] = [...messages];
|
|
387
|
+
|
|
388
|
+
// Filter empty text blocks (unless model requires preserving them)
|
|
389
|
+
if (!modelConfig?.preserveEmptyContentBlocks) {
|
|
390
|
+
processedMessages = processedMessages.map((msg) => {
|
|
391
|
+
if (
|
|
392
|
+
(msg.role === "user" || msg.role === "assistant") &&
|
|
393
|
+
Array.isArray(msg.content)
|
|
394
|
+
) {
|
|
395
|
+
const filteredContent = msg.content.filter((part: any) => {
|
|
396
|
+
if (part.type !== "text") return true;
|
|
397
|
+
return part.text && part.text.trim().length > 0;
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
...msg,
|
|
402
|
+
content: filteredContent.length > 0 ? filteredContent : "",
|
|
403
|
+
} as CoreMessage;
|
|
404
|
+
}
|
|
405
|
+
return msg;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Handle file attachments
|
|
410
|
+
if (fileAttachments && fileAttachments.length > 0) {
|
|
411
|
+
const lastMessage = processedMessages[processedMessages.length - 1];
|
|
412
|
+
if (lastMessage && lastMessage.role === "user") {
|
|
413
|
+
const textContent =
|
|
414
|
+
typeof lastMessage.content === "string" ? lastMessage.content : "";
|
|
415
|
+
|
|
416
|
+
const parts: any[] = textContent.trim()
|
|
417
|
+
? [{ type: "text", text: textContent }]
|
|
418
|
+
: [];
|
|
419
|
+
|
|
420
|
+
for (const attachment of fileAttachments) {
|
|
421
|
+
const mimeType = attachment.mimeType || (attachment as any).type;
|
|
422
|
+
|
|
423
|
+
if (!mimeType) {
|
|
424
|
+
parts.push({
|
|
425
|
+
type: "text",
|
|
426
|
+
text: `[Attached file: ${attachment.name} - type unknown]`,
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (mimeType.startsWith("image/")) {
|
|
432
|
+
parts.push({
|
|
433
|
+
type: "image",
|
|
434
|
+
image: attachment.data,
|
|
435
|
+
mimeType: mimeType,
|
|
436
|
+
});
|
|
437
|
+
} else if (
|
|
438
|
+
mimeType === "application/pdf" ||
|
|
439
|
+
mimeType.startsWith("text/") ||
|
|
440
|
+
mimeType.startsWith("application/")
|
|
441
|
+
) {
|
|
442
|
+
parts.push({
|
|
443
|
+
type: "file",
|
|
444
|
+
data: Buffer.from(attachment.data, "base64"),
|
|
445
|
+
mediaType: mimeType,
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
parts.push({
|
|
449
|
+
type: "text",
|
|
450
|
+
text: `[Attached file: ${attachment.name}]`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
processedMessages[processedMessages.length - 1] = {
|
|
456
|
+
...lastMessage,
|
|
457
|
+
content: parts,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return processedMessages;
|
|
463
|
+
}
|
|
464
|
+
}
|