Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
},
"devDependencies": {
"@jupyter/eslint-plugin": "^0.0.5",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"eslint": "^8.36.0",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.65.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^5.0.0",
"prettier": "^3.0.0",
Expand All @@ -55,7 +55,8 @@
".venv",
"webpack.config.js",
"python/jupyterlite-ai/jupyterlite_ai",
"python/jupyternaut-persona/jupyternaut_persona"
"python/jupyternaut-persona/jupyternaut_persona",
"**/style/index.js"
],
"extends": [
"eslint:recommended",
Expand Down Expand Up @@ -91,10 +92,16 @@
"args": "none"
}
],
"@typescript-eslint/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/quotes": [
"quotes": [
"error",
"single",
{
Expand Down
14 changes: 7 additions & 7 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
"sync:model-info": "node scripts/sync-model-info.mjs && prettier --write src/providers/generated-model-info.ts && eslint --fix src/providers/generated-model-info.ts"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.58",
"@ai-sdk/google": "^3.0.43",
"@ai-sdk/mcp": "^1.0.25",
"@ai-sdk/mistral": "^3.0.24",
"@ai-sdk/openai": "^3.0.41",
"@ai-sdk/openai-compatible": "^2.0.35",
"@ai-sdk/anthropic": "^4.0.37",
"@ai-sdk/google": "^4.0.41",
"@ai-sdk/mcp": "^2.0.30",
"@ai-sdk/mistral": "^4.0.28",
"@ai-sdk/openai": "^4.0.37",
"@ai-sdk/openai-compatible": "^3.0.29",
"@jupyterlab/apputils": "^4.6.8",
"@jupyterlab/coreutils": "^6.5.8",
"@jupyterlab/rendermime": "^4.5.8",
Expand All @@ -39,7 +39,7 @@
"@lumino/disposable": "^2.1.4",
"@lumino/signaling": "^2.1.4",
"@lumino/widgets": "^2.7.1",
"ai": "^6.0.116",
"ai": "^7.0.59",
"jupyter-secrets-manager": "^0.5.0",
"yaml": "^2.8.1",
"zod": "^4.3.6"
Expand Down
53 changes: 38 additions & 15 deletions packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
ToolLoopAgent,
type ModelMessage,
type LanguageModel,
stepCountIs,
type StreamTextResult,
isStepCount,
type SystemModelMessage,
type ToolApprovalRequestOutput,
type TypedToolError,
type TypedToolOutputDenied,
Expand All @@ -20,6 +20,7 @@ import {
import { ISecretsManager } from 'jupyter-secrets-manager';

import { createModel } from './providers/models';
import { createExecuteCommandApprovalPolicy } from './tools/commands';
import { getEffectiveContextWindow } from './providers/model-info';
import {
createProviderTools,
Expand Down Expand Up @@ -48,6 +49,13 @@ interface IMCPClientWrapper {
client: MCPClient;
}

/**
* The stream result type produced by the agent.
*/
type AgentStreamResult = Awaited<

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there any type defined in ai-sdk that would natively handle the stream result ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The native type seems to be StreamTextResult<ToolMap, Context, never> but Context is not exported. So we would need to copy that type here too which may make it drift over time.

ReturnType<ToolLoopAgent<never, ToolMap>['stream']>
>;

/**
* Result from processing a stream, including approval info if applicable.
*/
Expand Down Expand Up @@ -220,7 +228,11 @@ export class AgentManagerFactory implements IAgentManagerFactory {
const client = await createMCPClient({
transport: {
type: 'http',
url: serverConfig.url
url: serverConfig.url,
// The transport calls this option as a method (`this.fetchFn(...)`),
// so an unbound window.fetch would throw "Illegal invocation" in
// browsers.
fetch: globalThis.fetch.bind(globalThis)
}
});

Expand Down Expand Up @@ -592,10 +604,10 @@ export class AgentManager implements IAgentManager {

if (streamResult.aborted) {
try {
const responseMessages = await result.response;
if (responseMessages.messages?.length) {
const responseMessages = await result.responseMessages;
if (responseMessages.length) {
this._history.push(
...Private.sanitizeModelMessages(responseMessages.messages)
...Private.sanitizeModelMessages(responseMessages)
);
}
} catch {
Expand All @@ -605,11 +617,11 @@ export class AgentManager implements IAgentManager {
}

// Get response messages for completed steps.
const responseMessages = await result.response;
const responseMessages = await result.responseMessages;

// Add response messages to history
if (responseMessages.messages?.length) {
responseHistory.push(...responseMessages.messages);
if (responseMessages.length) {
responseHistory.push(...responseMessages);
}

// Add approval response if processed
Expand Down Expand Up @@ -678,11 +690,15 @@ export class AgentManager implements IAgentManager {
async textResponse(messages: ModelMessage[]): Promise<string> {
try {
const model = await this._createModel();
const instructions = messages.filter(
(message): message is SystemModelMessage => message.role === 'system'
);
const result = await generateText({
model,
messages
...(instructions.length > 0 && { instructions }),
messages: messages.filter(message => message.role !== 'system')
});
this._updateTokenUsage(result.totalUsage, result.totalUsage.inputTokens);
this._updateTokenUsage(result.usage, result.usage.inputTokens);
return result.text;
} catch (e) {
throw `Error while getting the topic of the chat\n${e}`;
Expand Down Expand Up @@ -917,7 +933,10 @@ ${richOutputWorkflowInstruction}`;
providerInfo
)
}),
stopWhen: stepCountIs(maxTurns)
stopWhen: isStepCount(maxTurns),
toolApproval: {
execute_command: createExecuteCommandApprovalPolicy(this._settingsModel)
}
});
}

Expand All @@ -928,7 +947,7 @@ ${richOutputWorkflowInstruction}`;
* @returns Processing result including approval info if applicable
*/
private async _processStreamResult(
result: StreamTextResult<ToolMap, never>
result: AgentStreamResult
): Promise<IStreamProcessResult> {
let fullResponse = '';
let currentMessageId: string | null = null;
Expand All @@ -937,7 +956,7 @@ ${richOutputWorkflowInstruction}`;
aborted: false
};

for await (const part of result.fullStream) {
for await (const part of result.stream) {
switch (part.type) {
case 'text-delta':
if (!currentMessageId) {
Expand All @@ -958,22 +977,26 @@ ${richOutputWorkflowInstruction}`;
});
break;

case 'tool-call':
case 'tool-call': {
// Complete current message before tool call
if (currentMessageId && fullResponse) {
this._emitMessageComplete(currentMessageId, fullResponse);
currentMessageId = null;
fullResponse = '';
}
const metadataTitle = part.toolMetadata?.title;
this._agentEvent.emit({
type: 'tool_call_start',
data: {
callId: part.toolCallId,
toolName: part.toolName,
title:
typeof metadataTitle === 'string' ? metadataTitle : part.title,
input: this._formatToolInput(JSON.stringify(part.input))
}
});
break;
}

case 'tool-result':
this._handleToolResult(part);
Expand Down
1 change: 1 addition & 0 deletions packages/agent/src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ export namespace IAgentManager {
tool_call_start: {
callId: string;
toolName: string;
title?: string;
input: string;
};
tool_call_complete: {
Expand Down
32 changes: 21 additions & 11 deletions packages/agent/src/tools/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function searchCommands(
*/
export function createDiscoverCommandsTool(commands: CommandRegistry): ITool {
return tool({
title: 'Discover Commands',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we still provide the title with the providerMetadata argument ? Or is this useless ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like title is now deprecated and the tools calls were not using it anyway (showing discover_commands instead).

But maybe metadata can work.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using metadata:

image

metadata: { title: 'Discover Commands' },
description:
'Discover all available JupyterLab commands with their metadata, arguments, and descriptions',
inputSchema: z.object({
Expand Down Expand Up @@ -143,15 +143,30 @@ export function createDiscoverCommandsTool(commands: CommandRegistry): ITool {
}

/**
* Create a tool to execute a specific JupyterLab command.
* Create the approval policy for the execute command tool, to be used with the
* `toolApproval` option of a `generateText`/`streamText` call or agent.
* Commands in the settings' commandsRequiringApproval list will need approval.
*/
export function createExecuteCommandTool(
commands: CommandRegistry,
export function createExecuteCommandApprovalPolicy(
settingsModel: IAISettingsModel
): ITool {
): (input: { commandId: string; args?: any }) => 'user-approval' | undefined {
return input => {
const commandsRequiringApproval =
settingsModel.config.commandsRequiringApproval || [];
return commandsRequiringApproval.includes(input.commandId)
? 'user-approval'
: undefined;
};
}

/**
* Create a tool to execute a specific JupyterLab command.
* Approval for commands in the settings' commandsRequiringApproval list is
* handled at the agent level via `createExecuteCommandApprovalPolicy`.
*/
export function createExecuteCommandTool(commands: CommandRegistry): ITool {
return tool({
title: 'Execute Command',
metadata: { title: 'Execute Command' },
description:
'Execute a specific JupyterLab command with optional arguments',
inputSchema: z.object({
Expand All @@ -163,11 +178,6 @@ export function createExecuteCommandTool(
'Optional arguments object to pass to the command (must be an object, not a string)'
)
}),
needsApproval: (input: { commandId: string; args?: any }) => {
const commandsRequiringApproval =
settingsModel.config.commandsRequiringApproval || [];
return commandsRequiringApproval.includes(input.commandId);
},
execute: async (input: { commandId: string; args?: any }) => {
const { commandId, args } = input;

Expand Down
4 changes: 2 additions & 2 deletions packages/agent/src/tools/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ISkillRegistry, ITool } from '../tokens';
*/
export function createDiscoverSkillsTool(skillRegistry: ISkillRegistry): ITool {
return tool({
title: 'Discover Skills',
metadata: { title: 'Discover Skills' },
description:
'Discover available agent skills with their names and descriptions',
inputSchema: z.object({
Expand All @@ -35,7 +35,7 @@ export function createDiscoverSkillsTool(skillRegistry: ISkillRegistry): ITool {
*/
export function createLoadSkillTool(skillRegistry: ISkillRegistry): ITool {
return tool({
title: 'Load Skill',
metadata: { title: 'Load Skill' },
description:
'Load a skill definition or a specific resource file bundled with a skill',
inputSchema: z.object({
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/tools/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async function readResponseText(
*/
export function createBrowserFetchTool(): ITool {
return tool({
title: 'Browser Fetch',
metadata: { title: 'Browser Fetch' },
description:
'Fetch a URL directly from the browser using HTTP GET for exact URL inspection when CORS/access permits.',
inputSchema: z.object({
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@lumino/signaling": "^2.1.4",
"@mui/icons-material": "^7",
"@mui/material": "^7",
"ai": "^6.0.116",
"ai": "^7.0.59",
"jupyter-chat-components": "^0.6.0",
"react": "^18.3.1"
},
Expand Down
6 changes: 5 additions & 1 deletion packages/ai/src/components/tool-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,11 @@ export function ToolSelect(props: IToolSelectProps): JSX.Element {
{tools.map(namedTool => (
<Tooltip
key={namedTool.name}
title={namedTool.tool.description || namedTool.name}
title={
typeof namedTool.tool.description === 'string'
? namedTool.tool.description
: namedTool.name
}
placement="left"
>
<MenuItem
Expand Down
9 changes: 5 additions & 4 deletions packages/ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,11 @@ const chatTracker: JupyterFrontEndPlugin<IChatTracker> = {
if (!agent) {
return;
}
const isApproved = optionId === 'approve';
isApproved
? agent.approveToolCall(toolCallId)
: agent.rejectToolCall(toolCallId);
if (optionId === 'approve') {
agent.approveToolCall(toolCallId);
} else {
agent.rejectToolCall(toolCallId);
}
}
};

Expand Down
2 changes: 1 addition & 1 deletion packages/persona/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@lumino/signaling": "^2.1.4",
"@mui/icons-material": "^7",
"@mui/material": "^7",
"ai": "^6.0.116",
"ai": "^7.0.59",
"jupyter-secrets-manager": "^0.5.0",
"react": "^18.3.1"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/persona/src/completion/completion-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class AICompletionProvider implements IInlineCompletionProvider {
const { text: completion } = await generateText({
model: this._model,
prompt: completionPrompt,
system: this.systemPrompt,
instructions: this.systemPrompt,
temperature: providerConfig.temperature || 0.3
});

Expand Down
12 changes: 2 additions & 10 deletions packages/persona/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,22 +471,14 @@ const toolRegistry: JupyterFrontEndPlugin<IToolRegistry> = {
id: '@jupyternaut/persona:tool-registry',
description: 'Provide the AI tool registry',
autoStart: true,
requires: [IAISettingsModel],
optional: [ISkillRegistry],
provides: IToolRegistry,
activate: (
app: JupyterFrontEnd,
settingsModel: IAISettingsModel,
skillRegistry?: ISkillRegistry
) => {
activate: (app: JupyterFrontEnd, skillRegistry?: ISkillRegistry) => {
const toolRegistry = new ToolRegistry();

// Add command operation tools
const discoverCommandsTool = createDiscoverCommandsTool(app.commands);
const executeCommandTool = createExecuteCommandTool(
app.commands,
settingsModel
);
const executeCommandTool = createExecuteCommandTool(app.commands);

toolRegistry.add('discover_commands', discoverCommandsTool);
toolRegistry.add('execute_command', executeCommandTool);
Expand Down
Loading
Loading