@jupyterlite/ai 0.11.1 → 0.13.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 +61 -7
- package/lib/agent.js +286 -103
- package/lib/chat-commands/clear.d.ts +8 -0
- package/lib/chat-commands/clear.js +30 -0
- package/lib/chat-commands/index.d.ts +2 -0
- package/lib/chat-commands/index.js +2 -0
- package/lib/chat-commands/skills.d.ts +19 -0
- package/lib/chat-commands/skills.js +57 -0
- 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 +16 -0
- package/lib/chat-model.js +191 -11
- 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 +311 -72
- package/lib/models/settings-model.d.ts +3 -0
- package/lib/models/settings-model.js +63 -14
- package/lib/providers/built-in-providers.js +12 -7
- 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/skills/index.d.ts +4 -0
- package/lib/skills/index.js +7 -0
- package/lib/skills/parse-skill.d.ts +25 -0
- package/lib/skills/parse-skill.js +69 -0
- package/lib/skills/skill-loader.d.ts +25 -0
- package/lib/skills/skill-loader.js +133 -0
- package/lib/skills/skill-registry.d.ts +31 -0
- package/lib/skills/skill-registry.js +100 -0
- package/lib/skills/types.d.ts +29 -0
- package/lib/skills/types.js +5 -0
- package/lib/tokens.d.ts +77 -7
- package/lib/tokens.js +6 -1
- package/lib/tools/commands.js +4 -2
- package/lib/tools/skills.d.ts +9 -0
- package/lib/tools/skills.js +73 -0
- package/lib/tools/web.d.ts +8 -0
- package/lib/tools/web.js +196 -0
- package/lib/widgets/ai-settings.d.ts +1 -1
- package/lib/widgets/ai-settings.js +157 -38
- 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 +207 -4
- package/package.json +18 -11
- package/schema/settings-model.json +97 -2
- package/src/agent.ts +397 -123
- package/src/chat-commands/clear.ts +46 -0
- package/src/chat-commands/index.ts +2 -0
- package/src/chat-commands/skills.ts +87 -0
- package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
- package/src/chat-model.ts +270 -23
- 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 +395 -87
- package/src/models/settings-model.ts +70 -15
- package/src/providers/built-in-providers.ts +12 -7
- package/src/providers/provider-tools.ts +179 -0
- package/src/rendered-message-outputarea.ts +62 -0
- package/src/skills/index.ts +14 -0
- package/src/skills/parse-skill.ts +91 -0
- package/src/skills/skill-loader.ts +175 -0
- package/src/skills/skill-registry.ts +137 -0
- package/src/skills/types.ts +37 -0
- package/src/tokens.ts +109 -9
- package/src/tools/commands.ts +4 -2
- package/src/tools/skills.ts +84 -0
- package/src/tools/web.ts +238 -0
- package/src/widgets/ai-settings.tsx +357 -77
- package/src/widgets/main-area-chat.ts +34 -1
- package/src/widgets/provider-config-dialog.tsx +496 -3
|
@@ -16,6 +16,7 @@ export class AISettingsModel extends VDomModel {
|
|
|
16
16
|
showCellDiff: true,
|
|
17
17
|
showFileDiff: true,
|
|
18
18
|
diffDisplayMode: 'split',
|
|
19
|
+
skillsPaths: ['.agents/skills', '_agents/skills'],
|
|
19
20
|
commandsRequiringApproval: [
|
|
20
21
|
'notebook:restart-run-all',
|
|
21
22
|
'notebook:run-cell',
|
|
@@ -32,6 +33,8 @@ export class AISettingsModel extends VDomModel {
|
|
|
32
33
|
'runmenu:run-all',
|
|
33
34
|
'jupyterlab-ai-commands:run-cell'
|
|
34
35
|
],
|
|
36
|
+
commandsAutoRenderMimeBundles: ['jupyterlab-ai-commands:execute-in-kernel'],
|
|
37
|
+
trustedMimeTypesForAutoRender: ['text/html'],
|
|
35
38
|
systemPrompt: `You are Jupyternaut, an AI coding assistant built specifically for the JupyterLab environment.
|
|
36
39
|
|
|
37
40
|
## Your Core Mission
|
|
@@ -51,7 +54,7 @@ You're designed to be a capable partner for data science, research, and developm
|
|
|
51
54
|
|
|
52
55
|
**⚡ Kernel Management:**
|
|
53
56
|
- Start new kernels with specified language or kernel name
|
|
54
|
-
- Execute code directly in
|
|
57
|
+
- Execute code directly in a kernel using jupyterlab-ai-commands execution commands (not console), without creating cells
|
|
55
58
|
- List running kernels and monitor their status
|
|
56
59
|
- Manage kernel lifecycle (start, monitor, shutdown)
|
|
57
60
|
|
|
@@ -68,18 +71,32 @@ You're designed to be a capable partner for data science, research, and developm
|
|
|
68
71
|
- Help with both quick fixes and long-term project planning
|
|
69
72
|
|
|
70
73
|
## How You Work
|
|
71
|
-
You
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
74
|
+
You interact with the user's JupyterLab environment primarily through the command system:
|
|
75
|
+
- Use 'discover_commands' to find available JupyterLab commands
|
|
76
|
+
- Use 'execute_command' to perform operations
|
|
77
|
+
- For file and notebook operations, use commands from the jupyterlab-ai-commands extension (prefixed with 'jupyterlab-ai-commands:')
|
|
78
|
+
- These commands provide comprehensive file and notebook manipulation: create, read, edit files/notebooks, manage cells, run code, etc.
|
|
79
|
+
- You can make systematic changes across multiple files and perform complex multi-step operations
|
|
80
|
+
- Skills are available via the skills tools: discover_skills (list) and load_skill (load instructions/resources)
|
|
81
|
+
|
|
82
|
+
## Tool & Skill Use Policy
|
|
83
|
+
- When tools or skills are available and the task requires actions or environment-specific facts, use them instead of guessing
|
|
84
|
+
- Never guess command IDs. Always use discover_commands with a relevant query before execute_command, unless you already discovered the command earlier in this conversation
|
|
85
|
+
- If a preloaded skills snapshot is provided in the system prompt, use it instead of calling discover_skills to list skills
|
|
86
|
+
- Only call discover_skills if the user explicitly asks for the latest list or you need to verify a skill not in the snapshot
|
|
87
|
+
- When a skill is relevant, call load_skill with the skill name to load instructions; if it returns a non-empty resources array, load each listed resource with load_skill before proceeding
|
|
88
|
+
- If you're unsure how to perform a request, discover relevant commands (discover_commands with task keywords)
|
|
89
|
+
- Use a relevant skill even when the user doesn't explicitly mention it
|
|
90
|
+
- Prefer the single most relevant tool or skill; if multiple could apply, ask a brief clarifying question
|
|
91
|
+
- Ask for missing required inputs before calling a tool or skill
|
|
92
|
+
- Before calling a tool or skill, briefly state why you're calling it
|
|
76
93
|
|
|
77
94
|
## Code Execution Strategy
|
|
78
95
|
When asked to run code or perform computations, choose the most appropriate approach:
|
|
79
|
-
- **For quick computations or one-off code execution**: Use kernel commands to
|
|
96
|
+
- **For quick computations or one-off code execution**: Use the kernel execution commands from jupyterlab-ai-commands to run code directly (no notebook/console). Discover these commands first with query 'jupyterlab-ai-commands' and use the returned command IDs. This is ideal for calculations, data lookups, or testing code snippets.
|
|
80
97
|
- **For work that should be saved**: Create or use notebooks when the user needs a persistent record of their work, wants to iterate on code, or is building something they'll return to later.
|
|
81
98
|
|
|
82
|
-
This means if the user asks you to "calculate the factorial of 100" or "check what library version is installed", run that directly
|
|
99
|
+
This means if the user asks you to "calculate the factorial of 100" or "check what library version is installed", run that directly with the jupyterlab-ai-commands kernel execution command rather than creating a new notebook file.
|
|
83
100
|
|
|
84
101
|
## Your Approach
|
|
85
102
|
- **Context-aware**: You understand the user is working in a data science/research environment
|
|
@@ -88,6 +105,23 @@ This means if the user asks you to "calculate the factorial of 100" or "check wh
|
|
|
88
105
|
- **Collaborative**: You are a pair programming partner, not just a code generator
|
|
89
106
|
|
|
90
107
|
## Communication Style & Agent Behavior
|
|
108
|
+
IMPORTANT: Follow this message flow pattern for better user experience:
|
|
109
|
+
|
|
110
|
+
1. FIRST: Explain what you're going to do and your approach
|
|
111
|
+
2. THEN: Execute tools (these will show automatically with step numbers)
|
|
112
|
+
3. FINALLY: Provide a concise summary of what was accomplished
|
|
113
|
+
|
|
114
|
+
Example flow:
|
|
115
|
+
- "I'll help you create a notebook with example cells. Let me first create the file structure, then add Python and Markdown cells."
|
|
116
|
+
- [Tool executions happen with automatic step display]
|
|
117
|
+
- "Successfully created your notebook with 3 cells: a title, code example, and visualization cell."
|
|
118
|
+
|
|
119
|
+
Guidelines:
|
|
120
|
+
- Start responses with your plan/approach before tool execution
|
|
121
|
+
- Let the system handle tool execution display (don't duplicate details)
|
|
122
|
+
- End with a brief summary of accomplishments
|
|
123
|
+
- Use natural, conversational tone throughout
|
|
124
|
+
|
|
91
125
|
- **Conversational**: You maintain a friendly, natural conversation flow throughout the interaction
|
|
92
126
|
- **Progress Updates**: You write brief progress messages between tool uses that appear directly in the conversation
|
|
93
127
|
- **No Filler**: You avoid empty acknowledgments like "Sounds good!" or "Okay, I will..." - you get straight to work
|
|
@@ -106,13 +140,28 @@ This means if the user asks you to "calculate the factorial of 100" or "check wh
|
|
|
106
140
|
- You keep users informed of progress while staying focused on the task
|
|
107
141
|
|
|
108
142
|
## Multi-Step Task Handling
|
|
109
|
-
When users request complex tasks
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
143
|
+
When users request complex tasks, you use the command system to accomplish them:
|
|
144
|
+
- For file and notebook operations, use discover_commands with query 'jupyterlab-ai-commands' to find the curated set of AI commands (~17 commands)
|
|
145
|
+
- For other JupyterLab operations (terminal, launcher, UI), use specific keywords like 'terminal', 'launcher', etc.
|
|
146
|
+
- IMPORTANT: Always use 'jupyterlab-ai-commands' as the query for file/notebook tasks - this returns a focused set instead of 100+ generic commands
|
|
147
|
+
- For example, to create a notebook with cells:
|
|
148
|
+
1. discover_commands with query 'jupyterlab-ai-commands' to find available file/notebook commands
|
|
149
|
+
2. execute_command with 'jupyterlab-ai-commands:create-notebook' and required arguments
|
|
150
|
+
3. execute_command with 'jupyterlab-ai-commands:add-cell' multiple times to add cells
|
|
151
|
+
4. execute_command with 'jupyterlab-ai-commands:set-cell-content' to add content to cells
|
|
152
|
+
5. execute_command with 'jupyterlab-ai-commands:run-cell' when appropriate
|
|
153
|
+
|
|
154
|
+
## Kernel Preference for Notebooks and Consoles
|
|
155
|
+
When creating notebooks or consoles for a specific programming language, use the 'kernelPreference' argument:
|
|
156
|
+
Only create consoles when the user explicitly asks for one; otherwise prefer the jupyterlab-ai-commands kernel execution commands for running code.
|
|
157
|
+
- To specify by language: { "kernelPreference": { "language": "python" } } or { "kernelPreference": { "language": "julia" } }
|
|
158
|
+
- To specify by kernel name: { "kernelPreference": { "name": "python3" } } or { "kernelPreference": { "name": "julia-1.10" } }
|
|
159
|
+
- Example: execute_command with commandId="notebook:create-new" and args={ "kernelPreference": { "language": "python" } }
|
|
160
|
+
- Example: execute_command with commandId="console:create" and args={ "kernelPreference": { "name": "python3" } }
|
|
161
|
+
- Common kernel names: "python3" (Python), "julia-1.10" (Julia), "ir" (R), "xpython" (xeus-python)
|
|
162
|
+
- If unsure of exact kernel name, prefer using "language" which will match any kernel supporting that language
|
|
114
163
|
|
|
115
|
-
Always think through multi-step tasks and use
|
|
164
|
+
Always think through multi-step tasks and use commands to fully complete the user's request rather than stopping after just one action.
|
|
116
165
|
|
|
117
166
|
You are ready to help users build something great!`,
|
|
118
167
|
// Completion system prompt - also defined in schema/settings-model.json
|
|
@@ -11,6 +11,7 @@ export const anthropicProvider = {
|
|
|
11
11
|
name: 'Anthropic Claude',
|
|
12
12
|
apiKeyRequirement: 'required',
|
|
13
13
|
defaultModels: [
|
|
14
|
+
'claude-opus-4-6',
|
|
14
15
|
'claude-opus-4-5',
|
|
15
16
|
'claude-opus-4-5-20251101',
|
|
16
17
|
'claude-sonnet-4-5',
|
|
@@ -31,6 +32,10 @@ export const anthropicProvider = {
|
|
|
31
32
|
],
|
|
32
33
|
supportsBaseURL: true,
|
|
33
34
|
supportsHeaders: true,
|
|
35
|
+
providerToolCapabilities: {
|
|
36
|
+
webSearch: { implementation: 'anthropic' },
|
|
37
|
+
webFetch: { implementation: 'anthropic' }
|
|
38
|
+
},
|
|
34
39
|
factory: (options) => {
|
|
35
40
|
if (!options.apiKey) {
|
|
36
41
|
throw new Error('API key required for Anthropic');
|
|
@@ -60,7 +65,7 @@ export const googleProvider = {
|
|
|
60
65
|
'gemini-3-flash-preview',
|
|
61
66
|
'gemini-2.5-pro',
|
|
62
67
|
'gemini-2.5-flash',
|
|
63
|
-
'gemini-2.5-flash-image
|
|
68
|
+
'gemini-2.5-flash-image',
|
|
64
69
|
'gemini-2.5-flash-lite',
|
|
65
70
|
'gemini-2.5-flash-lite-preview-09-2025',
|
|
66
71
|
'gemini-2.5-flash-preview-04-17',
|
|
@@ -154,21 +159,18 @@ export const openaiProvider = {
|
|
|
154
159
|
'gpt-5.2',
|
|
155
160
|
'gpt-5.2-chat-latest',
|
|
156
161
|
'gpt-5.2-pro',
|
|
162
|
+
'gpt-5.2-codex',
|
|
157
163
|
'gpt-5.1',
|
|
158
164
|
'gpt-5.1-chat-latest',
|
|
159
|
-
'gpt-5.1-codex',
|
|
160
|
-
'gpt-5.1-codex-mini',
|
|
161
|
-
'gpt-5.1-codex-max',
|
|
162
165
|
'gpt-5',
|
|
163
166
|
'gpt-5-2025-08-07',
|
|
164
167
|
'gpt-5-chat-latest',
|
|
165
|
-
'gpt-5-codex',
|
|
166
|
-
'gpt-5-pro',
|
|
167
|
-
'gpt-5-pro-2025-10-06',
|
|
168
168
|
'gpt-5-mini',
|
|
169
169
|
'gpt-5-mini-2025-08-07',
|
|
170
170
|
'gpt-5-nano',
|
|
171
171
|
'gpt-5-nano-2025-08-07',
|
|
172
|
+
'o4-mini',
|
|
173
|
+
'o4-mini-2025-04-16',
|
|
172
174
|
'o3',
|
|
173
175
|
'o3-2025-04-16',
|
|
174
176
|
'o3-mini',
|
|
@@ -200,6 +202,9 @@ export const openaiProvider = {
|
|
|
200
202
|
],
|
|
201
203
|
supportsBaseURL: true,
|
|
202
204
|
supportsHeaders: true,
|
|
205
|
+
providerToolCapabilities: {
|
|
206
|
+
webSearch: { implementation: 'openai' }
|
|
207
|
+
},
|
|
203
208
|
factory: (options) => {
|
|
204
209
|
if (!options.apiKey) {
|
|
205
210
|
throw new Error('API key required for OpenAI');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Tool } from 'ai';
|
|
2
|
+
import type { IProviderInfo } from '../tokens';
|
|
3
|
+
type ToolMap = Record<string, Tool>;
|
|
4
|
+
interface IWebSearchSettings {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
externalWebAccess?: boolean;
|
|
7
|
+
searchContextSize?: 'low' | 'medium' | 'high';
|
|
8
|
+
allowedDomains?: string[];
|
|
9
|
+
blockedDomains?: string[];
|
|
10
|
+
maxUses?: number;
|
|
11
|
+
}
|
|
12
|
+
interface IWebFetchSettings {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
maxUses?: number;
|
|
15
|
+
maxContentTokens?: number;
|
|
16
|
+
allowedDomains?: string[];
|
|
17
|
+
blockedDomains?: string[];
|
|
18
|
+
citationsEnabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Provider-level custom settings that control built-in web tools.
|
|
22
|
+
*/
|
|
23
|
+
export interface IProviderCustomSettings {
|
|
24
|
+
webSearch?: IWebSearchSettings;
|
|
25
|
+
webFetch?: IWebFetchSettings;
|
|
26
|
+
}
|
|
27
|
+
interface IProviderToolContext {
|
|
28
|
+
providerInfo?: IProviderInfo | null;
|
|
29
|
+
customSettings?: IProviderCustomSettings;
|
|
30
|
+
hasFunctionTools: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create provider-defined tools from custom settings and provider capabilities.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createProviderTools(options: IProviderToolContext): ToolMap;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
2
|
+
import { openai } from '@ai-sdk/openai';
|
|
3
|
+
const DEFAULT_ANTHROPIC_WEB_FETCH_MAX_USES = 2;
|
|
4
|
+
const DEFAULT_ANTHROPIC_WEB_FETCH_MAX_CONTENT_TOKENS = 12000;
|
|
5
|
+
function normalizeDomain(value) {
|
|
6
|
+
const normalized = (value || '').trim().toLowerCase();
|
|
7
|
+
const withoutProtocol = normalized.replace(/^https?:\/\//, '');
|
|
8
|
+
const hostname = withoutProtocol.split('/')[0].trim();
|
|
9
|
+
// Treat "*.example.com" as "example.com" for provider domain filters.
|
|
10
|
+
return hostname.startsWith('*.') ? hostname.slice(2) : hostname;
|
|
11
|
+
}
|
|
12
|
+
function collectDomains(value) {
|
|
13
|
+
value = value || [];
|
|
14
|
+
const values = Array.from(new Set(value.map(normalizeDomain).filter(domain => domain.length > 0)));
|
|
15
|
+
return values;
|
|
16
|
+
}
|
|
17
|
+
function createOpenAIWebSearchTool(webSearchSettings) {
|
|
18
|
+
const allowedDomains = collectDomains(webSearchSettings.allowedDomains);
|
|
19
|
+
return openai.tools.webSearch({
|
|
20
|
+
externalWebAccess: webSearchSettings.externalWebAccess,
|
|
21
|
+
searchContextSize: webSearchSettings.searchContextSize,
|
|
22
|
+
filters: allowedDomains.length > 0 ? { allowedDomains } : undefined
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function createAnthropicWebSearchTool(webSearchSettings) {
|
|
26
|
+
const allowedDomains = collectDomains(webSearchSettings.allowedDomains);
|
|
27
|
+
const blockedDomains = collectDomains(webSearchSettings.blockedDomains);
|
|
28
|
+
return anthropic.tools.webSearch_20250305({
|
|
29
|
+
maxUses: webSearchSettings.maxUses,
|
|
30
|
+
allowedDomains: allowedDomains.length > 0 ? allowedDomains : undefined,
|
|
31
|
+
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function createAnthropicWebFetchTool(webFetchSettings) {
|
|
35
|
+
const maxUses = webFetchSettings.maxUses ?? DEFAULT_ANTHROPIC_WEB_FETCH_MAX_USES;
|
|
36
|
+
const maxContentTokens = webFetchSettings.maxContentTokens ??
|
|
37
|
+
DEFAULT_ANTHROPIC_WEB_FETCH_MAX_CONTENT_TOKENS;
|
|
38
|
+
const allowedDomains = collectDomains(webFetchSettings.allowedDomains);
|
|
39
|
+
const blockedDomains = collectDomains(webFetchSettings.blockedDomains);
|
|
40
|
+
const citationsEnabled = webFetchSettings.citationsEnabled;
|
|
41
|
+
return anthropic.tools.webFetch_20250910({
|
|
42
|
+
maxUses,
|
|
43
|
+
maxContentTokens,
|
|
44
|
+
allowedDomains: allowedDomains.length > 0 ? allowedDomains : undefined,
|
|
45
|
+
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined,
|
|
46
|
+
citations: citationsEnabled !== undefined ? { enabled: citationsEnabled } : undefined
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function createWebSearchTool(implementation, webSearchSettings) {
|
|
50
|
+
switch (implementation) {
|
|
51
|
+
case 'openai':
|
|
52
|
+
return createOpenAIWebSearchTool(webSearchSettings);
|
|
53
|
+
case 'anthropic':
|
|
54
|
+
return createAnthropicWebSearchTool(webSearchSettings);
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unsupported web search implementation: ${implementation}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function createWebFetchTool(implementation, webFetchSettings) {
|
|
60
|
+
switch (implementation) {
|
|
61
|
+
case 'anthropic':
|
|
62
|
+
return createAnthropicWebFetchTool(webFetchSettings);
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Unsupported web fetch implementation: ${implementation}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create provider-defined tools from custom settings and provider capabilities.
|
|
69
|
+
*/
|
|
70
|
+
export function createProviderTools(options) {
|
|
71
|
+
const tools = {};
|
|
72
|
+
if (!options.customSettings ||
|
|
73
|
+
!options.providerInfo?.providerToolCapabilities) {
|
|
74
|
+
return tools;
|
|
75
|
+
}
|
|
76
|
+
const capabilities = options.providerInfo.providerToolCapabilities;
|
|
77
|
+
const webSearchSettings = options.customSettings.webSearch;
|
|
78
|
+
const webFetchSettings = options.customSettings.webFetch;
|
|
79
|
+
const webSearchEnabled = webSearchSettings?.enabled === true;
|
|
80
|
+
const webFetchEnabled = webFetchSettings?.enabled === true;
|
|
81
|
+
const webSearchCapability = capabilities.webSearch;
|
|
82
|
+
if (webSearchEnabled && webSearchSettings && webSearchCapability) {
|
|
83
|
+
const requiresNoFunctionTools = webSearchCapability.requiresNoFunctionTools === true;
|
|
84
|
+
if (!requiresNoFunctionTools || !options.hasFunctionTools) {
|
|
85
|
+
tools.web_search = createWebSearchTool(webSearchCapability.implementation, webSearchSettings);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const webFetchCapability = capabilities.webFetch;
|
|
89
|
+
if (webFetchEnabled && webFetchSettings && webFetchCapability) {
|
|
90
|
+
tools.web_fetch = createWebFetchTool(webFetchCapability.implementation, webFetchSettings);
|
|
91
|
+
}
|
|
92
|
+
return tools;
|
|
93
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ChatWidget } from '@jupyter/chat';
|
|
2
|
+
import { IDisposable } from '@lumino/disposable';
|
|
3
|
+
/**
|
|
4
|
+
* Ensures chat-rendered MIME outputs also expose the OutputArea class so
|
|
5
|
+
* renderer extensions can reuse their notebook/output-area CSS rules.
|
|
6
|
+
*
|
|
7
|
+
* TODO: Remove this compatibility layer once jupyter-chat applies
|
|
8
|
+
* `jp-OutputArea` (or equivalent output-area context) to rendered MIME
|
|
9
|
+
* messages by default.
|
|
10
|
+
*/
|
|
11
|
+
export declare class RenderedMessageOutputAreaCompat implements IDisposable {
|
|
12
|
+
constructor(options: RenderedMessageOutputAreaCompat.IOptions);
|
|
13
|
+
get isDisposed(): boolean;
|
|
14
|
+
dispose(): void;
|
|
15
|
+
private _scheduleSync;
|
|
16
|
+
private readonly _chatPanel;
|
|
17
|
+
private _isDisposed;
|
|
18
|
+
private _raf;
|
|
19
|
+
}
|
|
20
|
+
export declare namespace RenderedMessageOutputAreaCompat {
|
|
21
|
+
interface IOptions {
|
|
22
|
+
chatPanel: ChatWidget;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const OUTPUT_AREA_CLASS = 'jp-OutputArea';
|
|
2
|
+
const CHAT_RENDERED_MESSAGE_SELECTOR = `.jp-chat-rendered-message:not(.${OUTPUT_AREA_CLASS})`;
|
|
3
|
+
/**
|
|
4
|
+
* Ensures chat-rendered MIME outputs also expose the OutputArea class so
|
|
5
|
+
* renderer extensions can reuse their notebook/output-area CSS rules.
|
|
6
|
+
*
|
|
7
|
+
* TODO: Remove this compatibility layer once jupyter-chat applies
|
|
8
|
+
* `jp-OutputArea` (or equivalent output-area context) to rendered MIME
|
|
9
|
+
* messages by default.
|
|
10
|
+
*/
|
|
11
|
+
export class RenderedMessageOutputAreaCompat {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this._chatPanel = options.chatPanel;
|
|
14
|
+
this._chatPanel.model.messagesUpdated.connect(this._scheduleSync, this);
|
|
15
|
+
this._scheduleSync();
|
|
16
|
+
}
|
|
17
|
+
get isDisposed() {
|
|
18
|
+
return this._isDisposed;
|
|
19
|
+
}
|
|
20
|
+
dispose() {
|
|
21
|
+
if (this._isDisposed) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this._isDisposed = true;
|
|
25
|
+
this._chatPanel.model.messagesUpdated.disconnect(this._scheduleSync, this);
|
|
26
|
+
if (this._raf !== 0) {
|
|
27
|
+
cancelAnimationFrame(this._raf);
|
|
28
|
+
this._raf = 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
_scheduleSync() {
|
|
32
|
+
if (this._isDisposed || this._raf !== 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this._raf = requestAnimationFrame(() => {
|
|
36
|
+
this._raf = 0;
|
|
37
|
+
if (this._isDisposed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this._chatPanel.node
|
|
41
|
+
.querySelectorAll(CHAT_RENDERED_MESSAGE_SELECTOR)
|
|
42
|
+
.forEach(element => element.classList.add(OUTPUT_AREA_CLASS));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
_chatPanel;
|
|
46
|
+
_isDisposed = false;
|
|
47
|
+
_raf = 0;
|
|
48
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { parseSkillMd, type IParsedSkill } from './parse-skill';
|
|
2
|
+
export { loadSkillsFromPaths, type ISkillFileDefinition } from './skill-loader';
|
|
3
|
+
export type { ISkillDefinition, ISkillRegistration, ISkillResourceResult, ISkillSummary } from './types';
|
|
4
|
+
export { SkillRegistry } from './skill-registry';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
export { parseSkillMd } from './parse-skill';
|
|
6
|
+
export { loadSkillsFromPaths } from './skill-loader';
|
|
7
|
+
export { SkillRegistry } from './skill-registry';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed skill definition from a SKILL.md file.
|
|
3
|
+
*/
|
|
4
|
+
export interface IParsedSkill {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
instructions: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse a SKILL.md file content into a structured skill definition.
|
|
11
|
+
*
|
|
12
|
+
* Expected format:
|
|
13
|
+
* ```
|
|
14
|
+
* ---
|
|
15
|
+
* name: my-skill
|
|
16
|
+
* description: A brief description of the skill
|
|
17
|
+
* ---
|
|
18
|
+
* Full instructions body here...
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @param content - The raw content of a SKILL.md file
|
|
22
|
+
* @returns Parsed skill with name, description, and instructions
|
|
23
|
+
* @throws Error if the frontmatter is missing or invalid
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseSkillMd(content: string): IParsedSkill;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { parse as parseYaml } from 'yaml';
|
|
6
|
+
/**
|
|
7
|
+
* Parse a SKILL.md file content into a structured skill definition.
|
|
8
|
+
*
|
|
9
|
+
* Expected format:
|
|
10
|
+
* ```
|
|
11
|
+
* ---
|
|
12
|
+
* name: my-skill
|
|
13
|
+
* description: A brief description of the skill
|
|
14
|
+
* ---
|
|
15
|
+
* Full instructions body here...
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @param content - The raw content of a SKILL.md file
|
|
19
|
+
* @returns Parsed skill with name, description, and instructions
|
|
20
|
+
* @throws Error if the frontmatter is missing or invalid
|
|
21
|
+
*/
|
|
22
|
+
export function parseSkillMd(content) {
|
|
23
|
+
const normalizedContent = content
|
|
24
|
+
.replace(/^\uFEFF/, '')
|
|
25
|
+
.replace(/\r\n/g, '\n');
|
|
26
|
+
const lines = normalizedContent.split('\n');
|
|
27
|
+
if (lines[0]?.trim() !== '---') {
|
|
28
|
+
throw new Error('Invalid SKILL.md: missing frontmatter delimiters (---)');
|
|
29
|
+
}
|
|
30
|
+
const frontmatterLines = [];
|
|
31
|
+
let i = 1;
|
|
32
|
+
for (; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i];
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (trimmed === '---') {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
frontmatterLines.push(line);
|
|
39
|
+
}
|
|
40
|
+
if (i >= lines.length) {
|
|
41
|
+
throw new Error('Invalid SKILL.md: missing frontmatter delimiters (---)');
|
|
42
|
+
}
|
|
43
|
+
const frontmatter = frontmatterLines.join('\n');
|
|
44
|
+
const instructions = lines
|
|
45
|
+
.slice(i + 1)
|
|
46
|
+
.join('\n')
|
|
47
|
+
.trim();
|
|
48
|
+
let metadata;
|
|
49
|
+
try {
|
|
50
|
+
metadata = parseYaml(frontmatter);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error(`Invalid SKILL.md: YAML frontmatter parse failed: ${error}`);
|
|
54
|
+
}
|
|
55
|
+
const data = metadata;
|
|
56
|
+
const name = data?.name;
|
|
57
|
+
const description = data?.description;
|
|
58
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
59
|
+
throw new Error('Invalid SKILL.md: missing "name" in frontmatter');
|
|
60
|
+
}
|
|
61
|
+
if (typeof description !== 'string' || description.trim().length === 0) {
|
|
62
|
+
throw new Error('Invalid SKILL.md: missing "description" in frontmatter');
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
name: name.trim(),
|
|
66
|
+
description: description.trim(),
|
|
67
|
+
instructions
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Contents } from '@jupyterlab/services';
|
|
2
|
+
import { IParsedSkill } from './parse-skill';
|
|
3
|
+
/**
|
|
4
|
+
* A skill definition loaded from the filesystem.
|
|
5
|
+
*/
|
|
6
|
+
export interface ISkillFileDefinition extends IParsedSkill {
|
|
7
|
+
/**
|
|
8
|
+
* Path to the skill directory (e.g. ".agents/skills/my-skill").
|
|
9
|
+
*/
|
|
10
|
+
path: string;
|
|
11
|
+
/**
|
|
12
|
+
* Paths to resource files relative to the skill directory.
|
|
13
|
+
*/
|
|
14
|
+
resources: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Load skills from multiple directories. Each path is scanned in order;
|
|
18
|
+
* when the same skill name appears in more than one path, the first
|
|
19
|
+
* occurrence wins.
|
|
20
|
+
*
|
|
21
|
+
* @param contentsManager - The Jupyter contents manager
|
|
22
|
+
* @param skillsPaths - Ordered list of directories to scan
|
|
23
|
+
* @returns Merged array of loaded skill definitions
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadSkillsFromPaths(contentsManager: Contents.IManager, skillsPaths: string[]): Promise<ISkillFileDefinition[]>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
6
|
+
import { parseSkillMd } from './parse-skill';
|
|
7
|
+
/**
|
|
8
|
+
* Load skills from multiple directories. Each path is scanned in order;
|
|
9
|
+
* when the same skill name appears in more than one path, the first
|
|
10
|
+
* occurrence wins.
|
|
11
|
+
*
|
|
12
|
+
* @param contentsManager - The Jupyter contents manager
|
|
13
|
+
* @param skillsPaths - Ordered list of directories to scan
|
|
14
|
+
* @returns Merged array of loaded skill definitions
|
|
15
|
+
*/
|
|
16
|
+
export async function loadSkillsFromPaths(contentsManager, skillsPaths) {
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const merged = [];
|
|
19
|
+
for (const skillsPath of skillsPaths) {
|
|
20
|
+
const skills = await loadSkills(contentsManager, skillsPath);
|
|
21
|
+
for (const skill of skills) {
|
|
22
|
+
if (seen.has(skill.name)) {
|
|
23
|
+
console.debug(`Skipping duplicate skill "${skill.name}" from "${skillsPath}" (already loaded from an earlier path).`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
seen.add(skill.name);
|
|
27
|
+
merged.push(skill);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return merged;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Load skills from the filesystem by scanning a directory for subdirectories
|
|
34
|
+
* containing SKILL.md files.
|
|
35
|
+
*
|
|
36
|
+
* @param contentsManager - The Jupyter contents manager
|
|
37
|
+
* @param skillsPath - Path to the skills directory (e.g. ".agents/skills")
|
|
38
|
+
* @returns Array of loaded skill definitions
|
|
39
|
+
*/
|
|
40
|
+
async function loadSkills(contentsManager, skillsPath) {
|
|
41
|
+
const skills = [];
|
|
42
|
+
// Walk each path segment from root to verify the directory exists before fetching it.
|
|
43
|
+
const segments = skillsPath.split('/').filter(s => s.length > 0);
|
|
44
|
+
let currentPath = '';
|
|
45
|
+
for (const segment of segments) {
|
|
46
|
+
let listing;
|
|
47
|
+
try {
|
|
48
|
+
listing = await contentsManager.get(currentPath, { content: true });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.debug(`Skills path segment not found at "${currentPath}":`, error);
|
|
52
|
+
return skills;
|
|
53
|
+
}
|
|
54
|
+
const children = (listing.content ?? []);
|
|
55
|
+
if (!children.some(c => c.type === 'directory' && c.name === segment)) {
|
|
56
|
+
return skills;
|
|
57
|
+
}
|
|
58
|
+
currentPath = PathExt.join(currentPath, segment);
|
|
59
|
+
}
|
|
60
|
+
const dirModel = await contentsManager.get(skillsPath, { content: true });
|
|
61
|
+
if (dirModel.type !== 'directory' || !dirModel.content) {
|
|
62
|
+
return skills;
|
|
63
|
+
}
|
|
64
|
+
for (const child of dirModel.content) {
|
|
65
|
+
if (child.type !== 'directory') {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// List the subdirectory to check if SKILL.md exists before requesting it
|
|
69
|
+
const subDir = await contentsManager.get(child.path, { content: true });
|
|
70
|
+
const subChildren = (subDir.content ?? []);
|
|
71
|
+
if (!subChildren.some(f => f.type === 'file' && f.name === 'SKILL.md')) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const skillMdPath = `${child.path}/SKILL.md`;
|
|
75
|
+
const fileModel = await contentsManager.get(skillMdPath, {
|
|
76
|
+
content: true
|
|
77
|
+
});
|
|
78
|
+
if (typeof fileModel.content !== 'string') {
|
|
79
|
+
console.warn(`Skipping ${skillMdPath}: content is not a string`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const parsed = parseSkillMd(fileModel.content);
|
|
84
|
+
const resources = await collectResourcePaths(contentsManager, child.path);
|
|
85
|
+
skills.push({
|
|
86
|
+
...parsed,
|
|
87
|
+
path: child.path,
|
|
88
|
+
resources
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.warn(`Skipping skill at ${child.path}:`, error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return skills;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Recursively collect paths to all resource files in a skill directory,
|
|
99
|
+
* excluding `SKILL.md`. Content is loaded on-demand when the agent
|
|
100
|
+
* requests a specific resource.
|
|
101
|
+
*/
|
|
102
|
+
async function collectResourcePaths(contentsManager, basePath) {
|
|
103
|
+
const resourcePaths = [];
|
|
104
|
+
async function walk(dirPath) {
|
|
105
|
+
let dirModel;
|
|
106
|
+
try {
|
|
107
|
+
dirModel = await contentsManager.get(dirPath, { content: true });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.warn(`Failed to list directory ${dirPath}:`, error);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (dirModel.type !== 'directory' || !dirModel.content) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const item of dirModel.content) {
|
|
117
|
+
// Skip checkpoint directories
|
|
118
|
+
if (item.name === '.ipynb_checkpoints') {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (item.type === 'directory') {
|
|
122
|
+
await walk(item.path);
|
|
123
|
+
}
|
|
124
|
+
else if (item.type === 'file' && item.name !== 'SKILL.md') {
|
|
125
|
+
// Store path relative to the skill directory
|
|
126
|
+
const relativePath = PathExt.relative(basePath, item.path);
|
|
127
|
+
resourcePaths.push(relativePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
await walk(basePath);
|
|
132
|
+
return resourcePaths;
|
|
133
|
+
}
|