@jupyterlite/ai 0.12.0 → 0.14.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/lib/agent.d.ts +36 -2
- package/lib/agent.js +249 -24
- package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
- package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
- package/lib/chat-model.d.ts +8 -0
- package/lib/chat-model.js +156 -8
- package/lib/completion/completion-provider.d.ts +1 -1
- package/lib/completion/completion-provider.js +14 -2
- package/lib/components/model-select.js +4 -4
- package/lib/components/tool-select.d.ts +11 -2
- package/lib/components/tool-select.js +77 -18
- package/lib/index.d.ts +3 -3
- package/lib/index.js +249 -117
- package/lib/models/settings-model.d.ts +2 -0
- package/lib/models/settings-model.js +2 -0
- package/lib/providers/built-in-providers.js +33 -32
- package/lib/providers/provider-tools.d.ts +36 -0
- package/lib/providers/provider-tools.js +93 -0
- package/lib/rendered-message-outputarea.d.ts +24 -0
- package/lib/rendered-message-outputarea.js +48 -0
- package/lib/tokens.d.ts +65 -7
- package/lib/tokens.js +1 -1
- package/lib/tools/commands.js +62 -22
- package/lib/tools/web.d.ts +8 -0
- package/lib/tools/web.js +196 -0
- package/lib/widgets/ai-settings.d.ts +4 -9
- package/lib/widgets/ai-settings.js +123 -69
- package/lib/widgets/main-area-chat.d.ts +6 -0
- package/lib/widgets/main-area-chat.js +28 -0
- package/lib/widgets/provider-config-dialog.js +211 -11
- package/package.json +17 -11
- package/schema/settings-model.json +89 -1
- package/src/agent.ts +327 -42
- package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
- package/src/chat-model.ts +223 -14
- package/src/completion/completion-provider.ts +26 -12
- package/src/components/model-select.tsx +4 -5
- package/src/components/tool-select.tsx +110 -7
- package/src/index.ts +359 -184
- package/src/models/settings-model.ts +6 -0
- package/src/providers/built-in-providers.ts +33 -32
- package/src/providers/provider-tools.ts +179 -0
- package/src/rendered-message-outputarea.ts +62 -0
- package/src/tokens.ts +82 -9
- package/src/tools/commands.ts +99 -31
- package/src/tools/web.ts +238 -0
- package/src/widgets/ai-settings.tsx +279 -124
- package/src/widgets/main-area-chat.ts +34 -1
- package/src/widgets/provider-config-dialog.tsx +504 -11
package/src/agent.ts
CHANGED
|
@@ -7,14 +7,21 @@ import {
|
|
|
7
7
|
type StreamTextResult,
|
|
8
8
|
type Tool,
|
|
9
9
|
type ToolApprovalRequestOutput,
|
|
10
|
+
type TypedToolError,
|
|
11
|
+
type TypedToolOutputDenied,
|
|
10
12
|
type TypedToolResult
|
|
11
13
|
} from 'ai';
|
|
12
14
|
import { createMCPClient, type MCPClient } from '@ai-sdk/mcp';
|
|
13
15
|
import { ISecretsManager } from 'jupyter-secrets-manager';
|
|
16
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
14
17
|
|
|
15
18
|
import { AISettingsModel } from './models/settings-model';
|
|
16
19
|
import { createModel } from './providers/models';
|
|
17
|
-
import
|
|
20
|
+
import {
|
|
21
|
+
createProviderTools,
|
|
22
|
+
type IProviderCustomSettings
|
|
23
|
+
} from './providers/provider-tools';
|
|
24
|
+
import type { IProviderInfo, IProviderRegistry } from './tokens';
|
|
18
25
|
import {
|
|
19
26
|
ISkillRegistry,
|
|
20
27
|
ISkillSummary,
|
|
@@ -65,7 +72,7 @@ export namespace AgentManagerFactory {
|
|
|
65
72
|
/**
|
|
66
73
|
* The token used to request the secrets manager.
|
|
67
74
|
*/
|
|
68
|
-
token: symbol;
|
|
75
|
+
token: symbol | null;
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
export class AgentManagerFactory {
|
|
@@ -90,6 +97,11 @@ export class AgentManagerFactory {
|
|
|
90
97
|
|
|
91
98
|
// Listen for settings changes
|
|
92
99
|
this._settingsModel.stateChanged.connect(this._onSettingsChanged, this);
|
|
100
|
+
|
|
101
|
+
// Disable the secrets manager if the token is empty.
|
|
102
|
+
if (!options.token) {
|
|
103
|
+
this._secretsManager = undefined;
|
|
104
|
+
}
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
createAgent(options: IAgentManagerOptions): AgentManager {
|
|
@@ -99,6 +111,20 @@ export class AgentManagerFactory {
|
|
|
99
111
|
secretsManager: this._secretsManager
|
|
100
112
|
});
|
|
101
113
|
this._agentManagers.push(agentManager);
|
|
114
|
+
|
|
115
|
+
// New chats can be created before MCP setup finishes.
|
|
116
|
+
// Reinitialize them with connected MCP tools once it does.
|
|
117
|
+
this._initQueue
|
|
118
|
+
.then(() => this.getMCPTools())
|
|
119
|
+
.then(mcpTools => {
|
|
120
|
+
if (Object.keys(mcpTools).length > 0) {
|
|
121
|
+
agentManager.initializeAgent(mcpTools);
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
.catch(error =>
|
|
125
|
+
console.warn('Failed to pass MCP tools to new agent:', error)
|
|
126
|
+
);
|
|
127
|
+
|
|
102
128
|
return agentManager;
|
|
103
129
|
}
|
|
104
130
|
|
|
@@ -266,7 +292,7 @@ export interface IAgentEventTypeMap {
|
|
|
266
292
|
tool_call_complete: {
|
|
267
293
|
callId: string;
|
|
268
294
|
toolName: string;
|
|
269
|
-
|
|
295
|
+
outputData: unknown;
|
|
270
296
|
isError: boolean;
|
|
271
297
|
};
|
|
272
298
|
tool_approval_request: {
|
|
@@ -347,6 +373,11 @@ export interface IAgentManagerOptions {
|
|
|
347
373
|
* Initial token usage.
|
|
348
374
|
*/
|
|
349
375
|
tokenUsage?: ITokenUsage;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* JupyterLab render mime registry for discovering supported MIME types.
|
|
379
|
+
*/
|
|
380
|
+
renderMimeRegistry?: IRenderMimeRegistry;
|
|
350
381
|
}
|
|
351
382
|
|
|
352
383
|
/**
|
|
@@ -379,6 +410,7 @@ export class AgentManager {
|
|
|
379
410
|
this._tokenUsageChanged = new Signal<this, ITokenUsage>(this);
|
|
380
411
|
this._skills = [];
|
|
381
412
|
this._agentConfig = null;
|
|
413
|
+
this._renderMimeRegistry = options.renderMimeRegistry;
|
|
382
414
|
|
|
383
415
|
this.activeProvider =
|
|
384
416
|
options.activeProvider ?? this._settingsModel.config.defaultProvider;
|
|
@@ -607,7 +639,9 @@ export class AgentManager {
|
|
|
607
639
|
|
|
608
640
|
// Add response messages to history
|
|
609
641
|
if (responseMessages.messages?.length) {
|
|
610
|
-
this._history.push(
|
|
642
|
+
this._history.push(
|
|
643
|
+
...Private.sanitizeModelMessages(responseMessages.messages)
|
|
644
|
+
);
|
|
611
645
|
}
|
|
612
646
|
|
|
613
647
|
// Add approval response if processed
|
|
@@ -637,6 +671,9 @@ export class AgentManager {
|
|
|
637
671
|
data: { error: error as Error }
|
|
638
672
|
});
|
|
639
673
|
}
|
|
674
|
+
// After an error (including AbortError), sanitize the history
|
|
675
|
+
// to remove any trailing assistant messages without tool results
|
|
676
|
+
this._sanitizeHistory();
|
|
640
677
|
} finally {
|
|
641
678
|
this._controller = null;
|
|
642
679
|
}
|
|
@@ -697,21 +734,24 @@ export class AgentManager {
|
|
|
697
734
|
|
|
698
735
|
const model = await this._createModel();
|
|
699
736
|
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
this._toolRegistry &&
|
|
704
|
-
Object.keys(this._toolRegistry.tools).length > 0 &&
|
|
705
|
-
this._supportsToolCalling()
|
|
737
|
+
const supportsToolCalling = this._supportsToolCalling();
|
|
738
|
+
const canUseTools = config.toolsEnabled && supportsToolCalling;
|
|
739
|
+
const hasFunctionToolRegistry = !!(
|
|
740
|
+
this._toolRegistry && Object.keys(this._toolRegistry.tools).length > 0
|
|
706
741
|
);
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
742
|
+
const selectedFunctionTools =
|
|
743
|
+
canUseTools && hasFunctionToolRegistry ? this.selectedAgentTools : {};
|
|
744
|
+
const functionTools = canUseTools
|
|
745
|
+
? { ...selectedFunctionTools, ...this._mcpTools }
|
|
746
|
+
: {};
|
|
711
747
|
|
|
712
748
|
const activeProviderConfig = this._settingsModel.getProvider(
|
|
713
749
|
this._activeProvider
|
|
714
750
|
);
|
|
751
|
+
const activeProviderInfo =
|
|
752
|
+
activeProviderConfig && this._providerRegistry
|
|
753
|
+
? this._providerRegistry.getProviderInfo(activeProviderConfig.provider)
|
|
754
|
+
: null;
|
|
715
755
|
|
|
716
756
|
const temperature =
|
|
717
757
|
activeProviderConfig?.parameters?.temperature ?? DEFAULT_TEMPERATURE;
|
|
@@ -719,6 +759,15 @@ export class AgentManager {
|
|
|
719
759
|
const maxTurns =
|
|
720
760
|
activeProviderConfig?.parameters?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
721
761
|
|
|
762
|
+
const tools = this._buildRuntimeTools({
|
|
763
|
+
providerInfo: activeProviderInfo,
|
|
764
|
+
customSettings: activeProviderConfig?.customSettings,
|
|
765
|
+
functionTools,
|
|
766
|
+
includeProviderTools: canUseTools
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const shouldUseTools = canUseTools && Object.keys(tools).length > 0;
|
|
770
|
+
|
|
722
771
|
this._agentConfig = {
|
|
723
772
|
model,
|
|
724
773
|
tools,
|
|
@@ -730,6 +779,29 @@ export class AgentManager {
|
|
|
730
779
|
};
|
|
731
780
|
}
|
|
732
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Build the runtime tool map used by the agent.
|
|
784
|
+
*/
|
|
785
|
+
private _buildRuntimeTools(options: {
|
|
786
|
+
providerInfo?: IProviderInfo | null;
|
|
787
|
+
customSettings?: IProviderCustomSettings;
|
|
788
|
+
functionTools: ToolMap;
|
|
789
|
+
includeProviderTools: boolean;
|
|
790
|
+
}): ToolMap {
|
|
791
|
+
const providerTools = options.includeProviderTools
|
|
792
|
+
? createProviderTools({
|
|
793
|
+
providerInfo: options.providerInfo,
|
|
794
|
+
customSettings: options.customSettings,
|
|
795
|
+
hasFunctionTools: Object.keys(options.functionTools).length > 0
|
|
796
|
+
})
|
|
797
|
+
: {};
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
...providerTools,
|
|
801
|
+
...options.functionTools
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
733
805
|
/**
|
|
734
806
|
* Rebuild the agent using cached resources and the current skills snapshot.
|
|
735
807
|
*/
|
|
@@ -749,9 +821,22 @@ export class AgentManager {
|
|
|
749
821
|
shouldUseTools
|
|
750
822
|
} = this._agentConfig;
|
|
751
823
|
|
|
752
|
-
const
|
|
753
|
-
? this._getEnhancedSystemPrompt(baseSystemPrompt)
|
|
824
|
+
const baseInstructions = shouldUseTools
|
|
825
|
+
? this._getEnhancedSystemPrompt(baseSystemPrompt, tools)
|
|
754
826
|
: baseSystemPrompt || 'You are a helpful assistant.';
|
|
827
|
+
const richOutputWorkflowInstruction = shouldUseTools
|
|
828
|
+
? '- When the user asks for visual or rich outputs, prefer running code/commands that produce those outputs and describe that they will be rendered in chat.'
|
|
829
|
+
: '- When tools are unavailable, explain the limitation clearly and provide concrete steps the user can run to produce the desired rich outputs.';
|
|
830
|
+
const supportedMimeTypesInstruction =
|
|
831
|
+
this._getSupportedMimeTypesInstruction();
|
|
832
|
+
const instructions = `${baseInstructions}
|
|
833
|
+
|
|
834
|
+
RICH OUTPUT RENDERING:
|
|
835
|
+
- The chat UI can render rich MIME outputs as separate assistant messages.
|
|
836
|
+
- ${supportedMimeTypesInstruction}
|
|
837
|
+
- Use only MIME types from the supported list when creating MIME bundles. Do not invent MIME keys.
|
|
838
|
+
- Do not claim that you cannot display maps, images, or rich outputs in chat.
|
|
839
|
+
${richOutputWorkflowInstruction}`;
|
|
755
840
|
|
|
756
841
|
this._agent = new ToolLoopAgent({
|
|
757
842
|
model,
|
|
@@ -818,6 +903,14 @@ export class AgentManager {
|
|
|
818
903
|
this._handleToolResult(part);
|
|
819
904
|
break;
|
|
820
905
|
|
|
906
|
+
case 'tool-error':
|
|
907
|
+
this._handleToolError(part);
|
|
908
|
+
break;
|
|
909
|
+
|
|
910
|
+
case 'tool-output-denied':
|
|
911
|
+
this._handleToolOutputDenied(part);
|
|
912
|
+
break;
|
|
913
|
+
|
|
821
914
|
case 'tool-approval-request':
|
|
822
915
|
// Complete current message before approval
|
|
823
916
|
if (currentMessageId && fullResponse) {
|
|
@@ -856,10 +949,6 @@ export class AgentManager {
|
|
|
856
949
|
* Handles tool-result stream parts.
|
|
857
950
|
*/
|
|
858
951
|
private _handleToolResult(part: TypedToolResult<ToolMap>): void {
|
|
859
|
-
const output =
|
|
860
|
-
typeof part.output === 'string'
|
|
861
|
-
? part.output
|
|
862
|
-
: JSON.stringify(part.output, null, 2);
|
|
863
952
|
const isError =
|
|
864
953
|
typeof part.output === 'object' &&
|
|
865
954
|
part.output !== null &&
|
|
@@ -871,12 +960,49 @@ export class AgentManager {
|
|
|
871
960
|
data: {
|
|
872
961
|
callId: part.toolCallId,
|
|
873
962
|
toolName: part.toolName,
|
|
874
|
-
output,
|
|
963
|
+
outputData: part.output,
|
|
875
964
|
isError
|
|
876
965
|
}
|
|
877
966
|
});
|
|
878
967
|
}
|
|
879
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Handles tool-error stream parts.
|
|
971
|
+
*/
|
|
972
|
+
private _handleToolError(part: TypedToolError<ToolMap>): void {
|
|
973
|
+
const output =
|
|
974
|
+
typeof part.error === 'string'
|
|
975
|
+
? part.error
|
|
976
|
+
: part.error instanceof Error
|
|
977
|
+
? part.error.message
|
|
978
|
+
: JSON.stringify(part.error, null, 2);
|
|
979
|
+
|
|
980
|
+
this._agentEvent.emit({
|
|
981
|
+
type: 'tool_call_complete',
|
|
982
|
+
data: {
|
|
983
|
+
callId: part.toolCallId,
|
|
984
|
+
toolName: part.toolName,
|
|
985
|
+
outputData: output,
|
|
986
|
+
isError: true
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Handles tool-output-denied stream parts.
|
|
993
|
+
*/
|
|
994
|
+
private _handleToolOutputDenied(part: TypedToolOutputDenied<ToolMap>): void {
|
|
995
|
+
this._agentEvent.emit({
|
|
996
|
+
type: 'tool_call_complete',
|
|
997
|
+
data: {
|
|
998
|
+
callId: part.toolCallId,
|
|
999
|
+
toolName: part.toolName,
|
|
1000
|
+
outputData: 'Tool output was denied.',
|
|
1001
|
+
isError: true
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
880
1006
|
/**
|
|
881
1007
|
* Handles tool-approval-request stream parts.
|
|
882
1008
|
*/
|
|
@@ -980,14 +1106,23 @@ export class AgentManager {
|
|
|
980
1106
|
|
|
981
1107
|
let apiKey: string;
|
|
982
1108
|
if (this._secretsManager && this._settingsModel.config.useSecretsManager) {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1109
|
+
const token = Private.getToken();
|
|
1110
|
+
if (!token) {
|
|
1111
|
+
// This should never happen, the secrets manager should be disabled.
|
|
1112
|
+
console.error(
|
|
1113
|
+
'@jupyterlite/ai::AgentManager error: the settings manager token is not set.\nYou should disable the the secrets manager from the AI settings.'
|
|
1114
|
+
);
|
|
1115
|
+
apiKey = '';
|
|
1116
|
+
} else {
|
|
1117
|
+
apiKey =
|
|
1118
|
+
(
|
|
1119
|
+
await this._secretsManager.get(
|
|
1120
|
+
token,
|
|
1121
|
+
SECRETS_NAMESPACE,
|
|
1122
|
+
`${provider}:apiKey`
|
|
1123
|
+
)
|
|
1124
|
+
)?.value ?? '';
|
|
1125
|
+
}
|
|
991
1126
|
} else {
|
|
992
1127
|
apiKey = this._settingsModel.getApiKey(activeProviderConfig.id);
|
|
993
1128
|
}
|
|
@@ -1008,15 +1143,17 @@ export class AgentManager {
|
|
|
1008
1143
|
* @param baseSystemPrompt The base system prompt from settings
|
|
1009
1144
|
* @returns The enhanced system prompt with dynamic additions
|
|
1010
1145
|
*/
|
|
1011
|
-
private _getEnhancedSystemPrompt(
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1146
|
+
private _getEnhancedSystemPrompt(
|
|
1147
|
+
baseSystemPrompt: string,
|
|
1148
|
+
tools: ToolMap
|
|
1149
|
+
): string {
|
|
1150
|
+
let prompt = baseSystemPrompt;
|
|
1151
|
+
|
|
1152
|
+
if (this._skills.length > 0) {
|
|
1153
|
+
const lines = this._skills.map(
|
|
1154
|
+
skill => `- ${skill.name}: ${skill.description}`
|
|
1155
|
+
);
|
|
1156
|
+
const skillsPrompt = `
|
|
1020
1157
|
|
|
1021
1158
|
AGENT SKILLS:
|
|
1022
1159
|
Skills are provided via the skills registry and accessed through tools (not commands).
|
|
@@ -1028,8 +1165,137 @@ If the load_skill result includes a non-empty "resources" array, those are bundl
|
|
|
1028
1165
|
AVAILABLE SKILLS (preloaded snapshot):
|
|
1029
1166
|
${lines.join('\n')}
|
|
1030
1167
|
`;
|
|
1168
|
+
prompt += skillsPrompt;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const toolNames = new Set(Object.keys(tools));
|
|
1172
|
+
const hasBrowserFetch = toolNames.has('browser_fetch');
|
|
1173
|
+
const hasWebFetch = toolNames.has('web_fetch');
|
|
1174
|
+
const hasWebSearch = toolNames.has('web_search');
|
|
1175
|
+
|
|
1176
|
+
if (hasBrowserFetch || hasWebFetch || hasWebSearch) {
|
|
1177
|
+
const webRetrievalPrompt = `
|
|
1178
|
+
|
|
1179
|
+
WEB RETRIEVAL POLICY:
|
|
1180
|
+
- If the user asks about a specific URL and browser_fetch is available, call browser_fetch first for that URL.
|
|
1181
|
+
- If browser_fetch fails due to CORS/network/access, try web_fetch (if available) for that same URL.
|
|
1182
|
+
- If web_fetch fails with access/policy errors (for example: url_not_accessible or url_not_allowed) and browser_fetch is available, you MUST call browser_fetch for that same URL before searching.
|
|
1183
|
+
- If either fetch method fails with temporary access/network issues (for example: network_or_cors), try the other fetch method if available before searching.
|
|
1184
|
+
- Only fall back to web_search after both fetch methods fail or are unavailable.
|
|
1185
|
+
- If the user explicitly asks to inspect one exact URL, do not skip directly to search unless both fetch methods fail or are unavailable.
|
|
1186
|
+
- In your final response, state which retrieval method succeeded (browser_fetch, web_fetch, or web_search) and mention relevant limitations.
|
|
1187
|
+
`;
|
|
1188
|
+
prompt += webRetrievalPrompt;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return prompt;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Build an instruction line describing MIME types supported by this session.
|
|
1196
|
+
*/
|
|
1197
|
+
private _getSupportedMimeTypesInstruction(): string {
|
|
1198
|
+
const mimeTypes = this._renderMimeRegistry?.mimeTypes ?? [];
|
|
1199
|
+
const safeMimeTypes = mimeTypes.filter(mimeType => {
|
|
1200
|
+
const factory = this._renderMimeRegistry?.getFactory(mimeType);
|
|
1201
|
+
return !!factory?.safe;
|
|
1202
|
+
});
|
|
1031
1203
|
|
|
1032
|
-
|
|
1204
|
+
if (safeMimeTypes.length === 0) {
|
|
1205
|
+
return 'Supported MIME types are determined by the active JupyterLab renderers in this session.';
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
return `Supported MIME types in this session: ${safeMimeTypes.join(', ')}`;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Sanitizes history to ensure it's in a valid state in case of abort or error.
|
|
1213
|
+
*/
|
|
1214
|
+
private _sanitizeHistory(): void {
|
|
1215
|
+
if (this._history.length === 0) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const newHistory: ModelMessage[] = [];
|
|
1220
|
+
for (let i = 0; i < this._history.length; i++) {
|
|
1221
|
+
const msg = this._history[i];
|
|
1222
|
+
|
|
1223
|
+
if (msg.role === 'assistant') {
|
|
1224
|
+
const toolCallIds = this._getToolCallIds(msg);
|
|
1225
|
+
if (toolCallIds.length > 0) {
|
|
1226
|
+
// Find if there's a following tool message with results for these calls
|
|
1227
|
+
const nextMsg = this._history[i + 1];
|
|
1228
|
+
if (
|
|
1229
|
+
nextMsg &&
|
|
1230
|
+
nextMsg.role === 'tool' &&
|
|
1231
|
+
this._matchesAllToolCalls(nextMsg, toolCallIds)
|
|
1232
|
+
) {
|
|
1233
|
+
newHistory.push(msg);
|
|
1234
|
+
} else {
|
|
1235
|
+
// Message has unmatched tool calls drop it and everything after it
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
} else {
|
|
1239
|
+
newHistory.push(msg);
|
|
1240
|
+
}
|
|
1241
|
+
} else if (msg.role === 'tool') {
|
|
1242
|
+
// Tool messages are valid if they were preceded by a valid assistant message
|
|
1243
|
+
newHistory.push(msg);
|
|
1244
|
+
} else {
|
|
1245
|
+
newHistory.push(msg);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
this._history = newHistory;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Extracts tool call IDs from a message
|
|
1254
|
+
*/
|
|
1255
|
+
private _getToolCallIds(message: ModelMessage): string[] {
|
|
1256
|
+
const ids: string[] = [];
|
|
1257
|
+
|
|
1258
|
+
// Check content array for tool-call parts
|
|
1259
|
+
if (Array.isArray(message.content)) {
|
|
1260
|
+
for (const part of message.content) {
|
|
1261
|
+
if (
|
|
1262
|
+
typeof part === 'object' &&
|
|
1263
|
+
part !== null &&
|
|
1264
|
+
'type' in part &&
|
|
1265
|
+
part.type === 'tool-call'
|
|
1266
|
+
) {
|
|
1267
|
+
ids.push(part.toolCallId);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
return ids;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Checks if a tool message contains results for all specified tool call IDs
|
|
1277
|
+
*/
|
|
1278
|
+
private _matchesAllToolCalls(
|
|
1279
|
+
message: ModelMessage,
|
|
1280
|
+
callIds: string[]
|
|
1281
|
+
): boolean {
|
|
1282
|
+
if (message.role !== 'tool' || !Array.isArray(message.content)) {
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const resultIds = new Set<string>();
|
|
1287
|
+
for (const part of message.content) {
|
|
1288
|
+
if (
|
|
1289
|
+
typeof part === 'object' &&
|
|
1290
|
+
part !== null &&
|
|
1291
|
+
'type' in part &&
|
|
1292
|
+
part.type === 'tool-result'
|
|
1293
|
+
) {
|
|
1294
|
+
resultIds.add(part.toolCallId);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return callIds.every(id => resultIds.has(id));
|
|
1033
1299
|
}
|
|
1034
1300
|
|
|
1035
1301
|
// Private attributes
|
|
@@ -1049,6 +1315,7 @@ ${lines.join('\n')}
|
|
|
1049
1315
|
private _activeProvider: string = '';
|
|
1050
1316
|
private _activeProviderChanged = new Signal<this, string | undefined>(this);
|
|
1051
1317
|
private _skills: ISkillSummary[];
|
|
1318
|
+
private _renderMimeRegistry?: IRenderMimeRegistry;
|
|
1052
1319
|
private _initQueue: Promise<void> = Promise.resolve();
|
|
1053
1320
|
private _agentConfig: IAgentConfig | null;
|
|
1054
1321
|
private _pendingApprovals: Map<
|
|
@@ -1058,14 +1325,32 @@ ${lines.join('\n')}
|
|
|
1058
1325
|
}
|
|
1059
1326
|
|
|
1060
1327
|
namespace Private {
|
|
1328
|
+
/**
|
|
1329
|
+
* Keep only serializable messages by doing a JSON round-trip.
|
|
1330
|
+
* Messages that cannot be serialized are dropped.
|
|
1331
|
+
*/
|
|
1332
|
+
export const sanitizeModelMessages = (
|
|
1333
|
+
messages: ModelMessage[]
|
|
1334
|
+
): ModelMessage[] => {
|
|
1335
|
+
const sanitized: ModelMessage[] = [];
|
|
1336
|
+
for (const message of messages) {
|
|
1337
|
+
try {
|
|
1338
|
+
sanitized.push(JSON.parse(JSON.stringify(message)));
|
|
1339
|
+
} catch {
|
|
1340
|
+
// Drop messages that cannot be serialized
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return sanitized;
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1061
1346
|
/**
|
|
1062
1347
|
* The token to use with the secrets manager, setter and getter.
|
|
1063
1348
|
*/
|
|
1064
|
-
let secretsToken: symbol;
|
|
1065
|
-
export function setToken(value: symbol): void {
|
|
1349
|
+
let secretsToken: symbol | null;
|
|
1350
|
+
export function setToken(value: symbol | null): void {
|
|
1066
1351
|
secretsToken = value;
|
|
1067
1352
|
}
|
|
1068
|
-
export function getToken(): symbol {
|
|
1353
|
+
export function getToken(): symbol | null {
|
|
1069
1354
|
return secretsToken;
|
|
1070
1355
|
}
|
|
1071
1356
|
}
|
|
@@ -4,31 +4,32 @@ import { AgentManagerFactory } from './agent';
|
|
|
4
4
|
import { AIChatModel } from './chat-model';
|
|
5
5
|
import { AISettingsModel } from './models/settings-model';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
IChatModelHandler,
|
|
8
8
|
IProviderRegistry,
|
|
9
9
|
ITokenUsage,
|
|
10
10
|
IToolRegistry
|
|
11
11
|
} from './tokens';
|
|
12
12
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
13
|
-
import {
|
|
13
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* The chat model
|
|
16
|
+
* The chat model handler.
|
|
17
17
|
*/
|
|
18
|
-
export class
|
|
19
|
-
constructor(options:
|
|
18
|
+
export class ChatModelHandler implements IChatModelHandler {
|
|
19
|
+
constructor(options: ChatModelHandler.IOptions) {
|
|
20
20
|
this._docManager = options.docManager;
|
|
21
21
|
this._agentManagerFactory = options.agentManagerFactory;
|
|
22
22
|
this._settingsModel = options.settingsModel;
|
|
23
23
|
this._toolRegistry = options.toolRegistry;
|
|
24
24
|
this._providerRegistry = options.providerRegistry;
|
|
25
|
+
this._rmRegistry = options.rmRegistry;
|
|
25
26
|
this._activeCellManager = options.activeCellManager;
|
|
26
27
|
this._trans = options.trans;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
createModel(
|
|
30
|
-
name
|
|
31
|
-
activeProvider
|
|
31
|
+
name: string,
|
|
32
|
+
activeProvider: string,
|
|
32
33
|
tokenUsage?: ITokenUsage
|
|
33
34
|
): AIChatModel {
|
|
34
35
|
// Create Agent Manager first so it can be shared
|
|
@@ -37,7 +38,8 @@ export class ChatModelRegistry implements IChatModelRegistry {
|
|
|
37
38
|
toolRegistry: this._toolRegistry,
|
|
38
39
|
providerRegistry: this._providerRegistry,
|
|
39
40
|
activeProvider,
|
|
40
|
-
tokenUsage
|
|
41
|
+
tokenUsage,
|
|
42
|
+
renderMimeRegistry: this._rmRegistry
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
// Create AI chat model
|
|
@@ -50,52 +52,11 @@ export class ChatModelRegistry implements IChatModelRegistry {
|
|
|
50
52
|
trans: this._trans
|
|
51
53
|
});
|
|
52
54
|
|
|
53
|
-
// Set the name of the chat if not provided.
|
|
54
|
-
// The name will be the name set by the user to the model if not already used by
|
|
55
|
-
// another chat.
|
|
56
|
-
if (!name || this._models.findIndex(m => m.name === name) !== -1) {
|
|
57
|
-
const existingName = this.getAll().map(model => model.name);
|
|
58
|
-
|
|
59
|
-
const modelName =
|
|
60
|
-
this._settingsModel.getProvider(agentManager.activeProvider)?.name ||
|
|
61
|
-
UUID.uuid4();
|
|
62
|
-
name = modelName;
|
|
63
|
-
let i = 1;
|
|
64
|
-
while (existingName.includes(name)) {
|
|
65
|
-
name = `${modelName}-${i}`;
|
|
66
|
-
i += 1;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
55
|
model.name = name;
|
|
70
|
-
this.add(model);
|
|
71
56
|
|
|
72
57
|
return model;
|
|
73
58
|
}
|
|
74
59
|
|
|
75
|
-
add(model: AIChatModel): void {
|
|
76
|
-
if (!this._models.find(m => m.name === model.name)) {
|
|
77
|
-
this._models.push(model);
|
|
78
|
-
model.disposed.connect(() => {
|
|
79
|
-
this.remove(model.name);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
get(name: string): AIChatModel | undefined {
|
|
85
|
-
return this._models.find(m => m.name === name);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
getAll(): AIChatModel[] {
|
|
89
|
-
return this._models;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
remove(name: string): void {
|
|
93
|
-
const index = this._models.findIndex(m => m.name === name);
|
|
94
|
-
if (index !== -1) {
|
|
95
|
-
this._models.splice(index, 1);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
60
|
/**
|
|
100
61
|
* Getter/setter for the active cell manager.
|
|
101
62
|
*/
|
|
@@ -106,17 +67,17 @@ export class ChatModelRegistry implements IChatModelRegistry {
|
|
|
106
67
|
this._activeCellManager = manager;
|
|
107
68
|
}
|
|
108
69
|
|
|
109
|
-
private _models: AIChatModel[] = [];
|
|
110
70
|
private _docManager: IDocumentManager;
|
|
111
71
|
private _agentManagerFactory: AgentManagerFactory;
|
|
112
72
|
private _settingsModel: AISettingsModel;
|
|
113
73
|
private _toolRegistry?: IToolRegistry;
|
|
114
74
|
private _providerRegistry?: IProviderRegistry;
|
|
75
|
+
private _rmRegistry: IRenderMimeRegistry;
|
|
115
76
|
private _activeCellManager?: ActiveCellManager;
|
|
116
77
|
private _trans: TranslationBundle;
|
|
117
78
|
}
|
|
118
79
|
|
|
119
|
-
export namespace
|
|
80
|
+
export namespace ChatModelHandler {
|
|
120
81
|
export interface IOptions {
|
|
121
82
|
/**
|
|
122
83
|
* The document manager.
|
|
@@ -138,6 +99,10 @@ export namespace ChatModelRegistry {
|
|
|
138
99
|
* Optional provider registry for model creation
|
|
139
100
|
*/
|
|
140
101
|
providerRegistry?: IProviderRegistry;
|
|
102
|
+
/**
|
|
103
|
+
* Render mime registry.
|
|
104
|
+
*/
|
|
105
|
+
rmRegistry: IRenderMimeRegistry;
|
|
141
106
|
/**
|
|
142
107
|
* The active cell manager.
|
|
143
108
|
*/
|