From 992aa1223d9271158b08618c880b83eedc6fa6d6 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 26 Jun 2025 17:18:02 +0200 Subject: [PATCH 01/10] Include langgraph to call tool from the chat prompt --- package.json | 1 + schema/provider-registry.json | 6 ++ schema/system-prompts.json | 2 +- src/chat-handler.ts | 143 +++++++++++++++++++++++++++------- src/index.ts | 6 ++ src/provider.ts | 66 ++++++++++++++++ src/tokens.ts | 13 +++- src/tools/test_tool.ts | 18 +++++ yarn.lock | 55 ++++++++++++- 9 files changed, 277 insertions(+), 33 deletions(-) create mode 100644 src/tools/test_tool.ts diff --git a/package.json b/package.json index 4180e95..2bf60c0 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@langchain/community": "^0.3.48", "@langchain/core": "^0.3.62", "@langchain/google-genai": "^0.2.14", + "@langchain/langgraph": "^0.3.5", "@langchain/mistralai": "^0.2.1", "@langchain/ollama": "^0.2.3", "@langchain/openai": "^0.5.16", diff --git a/schema/provider-registry.json b/schema/provider-registry.json index ad5cfe9..fc54672 100644 --- a/schema/provider-registry.json +++ b/schema/provider-registry.json @@ -17,6 +17,12 @@ "description": "Whether to use only one provider for the chat and the completer.\nThis will overwrite all the settings for the completer, and copy the ones from the chat.", "default": true }, + "UseAgent": { + "type": "boolean", + "title": "Use agent in chat", + "description": "Whether to use or not an agent in chat", + "default": false + }, "AIproviders": { "type": "object", "title": "AI providers", diff --git a/schema/system-prompts.json b/schema/system-prompts.json index 42d30c7..03ccefd 100644 --- a/schema/system-prompts.json +++ b/schema/system-prompts.json @@ -9,7 +9,7 @@ "type": "string", "title": "Chat message system prompt", "description": "The system prompt for the chat messages", - "default": "You are Jupyternaut, a conversational assistant living in JupyterLab to help users.\nYou are not a language model, but rather an application built on a foundation model from $provider_name$.\nYou are talkative and you provide lots of specific details from the foundation model's context.\nYou may use Markdown to format your response.\nIf your response includes code, they must be enclosed in Markdown fenced code blocks (with triple backticks before and after).\nIf your response includes mathematical notation, they must be expressed in LaTeX markup and enclosed in LaTeX delimiters.\nAll dollar quantities (of USD) must be formatted in LaTeX, with the `$` symbol escaped by a single backslash `\\`.\n- Example prompt: `If I have \\\\$100 and spend \\\\$20, how much money do I have left?`\n- **Correct** response: `You have \\(\\$80\\) remaining.`\n- **Incorrect** response: `You have $80 remaining.`\nIf you do not know the answer to a question, answer truthfully by responding that you do not know.\nThe following is a friendly conversation between you and a human." + "default": "You are Jupyternaut, a conversational assistant living in JupyterLab to help users.\nYou are not a language model, but rather an application built on a foundation model from $provider_name$.\nYou are talkative and you provide lots of specific details from the foundation model's context.\nYou may use Markdown to format your response.\nIf your response includes code, they must be enclosed in Markdown fenced code blocks (with triple backticks before and after).\nIf your response includes mathematical notation, they must be expressed in LaTeX markup and enclosed in LaTeX delimiters.\nAll dollar quantities (of USD) must be formatted in LaTeX, with the `$` symbol escaped by a single backslash `\\`.\n- Example prompt: `If I have \\\\$100 and spend \\\\$20, how much money do I have left?`\n- **Correct** response: `You have \\(\\$80\\) remaining.`\n- **Incorrect** response: `You have $80 remaining.`\nIf you do not know the answer to a question, answer truthfully by responding that you do not know.\nAlways use the tool to answer, if available.\nThe following is a friendly conversation between you and a human." }, "completionSystemPrompt": { "type": "string", diff --git a/src/chat-handler.ts b/src/chat-handler.ts index 4d10fe5..d34fbde 100644 --- a/src/chat-handler.ts +++ b/src/chat-handler.ts @@ -13,14 +13,17 @@ import { IChatMessage, IChatModel, IInputModel, - INewMessage + INewMessage, + IUser } from '@jupyter/chat'; import { AIMessage, + BaseMessage, HumanMessage, mergeMessageRuns, SystemMessage } from '@langchain/core/messages'; +import { CompiledStateGraph } from '@langchain/langgraph'; import { UUID } from '@lumino/coreutils'; import { DEFAULT_CHAT_SYSTEM_PROMPT } from './default-prompts'; @@ -62,7 +65,11 @@ export class ChatHandler extends AbstractChatModel { }); } - get provider(): AIChatModel | null { + get agent(): CompiledStateGraph | null { + return this._providerRegistry.currentAgent; + } + + get chatModel(): AIChatModel | null { return this._providerRegistry.currentChatModel; } @@ -110,7 +117,9 @@ export class ChatHandler extends AbstractChatModel { }; this.messageAdded(msg); - if (this._providerRegistry.currentChatModel === null) { + const chatModel = this.chatModel; + + if (chatModel === null) { const errorMsg: IChatMessage = { id: UUID.uuid4(), body: `**${this._errorMessage ? this._errorMessage : this._defaultErrorMessage}**`, @@ -137,7 +146,39 @@ export class ChatHandler extends AbstractChatModel { const sender = { username: this._personaName, avatar_url: AI_AVATAR }; this.updateWriters([{ user: sender }]); - // create an empty message to be filled by the AI provider + if (this._providerRegistry.useAgent && this.agent !== null) { + return this._sendAgentMessage(this.agent, messages, sender); + } + + return this._sentChatMessage(chatModel, messages, sender); + } + + async getHistory(): Promise { + return this._history; + } + + dispose(): void { + super.dispose(); + } + + messageAdded(message: IChatMessage): void { + super.messageAdded(message); + } + + stopStreaming(): void { + this._controller?.abort(); + } + + createChatContext(): IChatContext { + return new ChatHandler.ChatContext({ model: this }); + } + + private async _sentChatMessage( + chatModel: AIChatModel, + messages: BaseMessage[], + sender: IUser + ): Promise { + // Create an empty message to be filled by the AI provider const botMsg: IChatMessage = { id: UUID.uuid4(), body: '', @@ -145,15 +186,12 @@ export class ChatHandler extends AbstractChatModel { time: Private.getTimestampMs(), type: 'msg' }; - let content = ''; - this._controller = new AbortController(); try { - for await (const chunk of await this._providerRegistry.currentChatModel.stream( - messages, - { signal: this._controller.signal } - )) { + for await (const chunk of await chatModel.stream(messages, { + signal: this._controller.signal + })) { content += chunk.content ?? chunk; botMsg.body = content; this.messageAdded(botMsg); @@ -177,26 +215,73 @@ export class ChatHandler extends AbstractChatModel { } } - async getHistory(): Promise { - return this._history; - } - - dispose(): void { - super.dispose(); - } - - messageAdded(message: IChatMessage): void { - super.messageAdded(message); - } - - stopStreaming(): void { - this._controller?.abort(); - } - - createChatContext(): IChatContext { - return new ChatHandler.ChatContext({ model: this }); + private async _sendAgentMessage( + agent: CompiledStateGraph, + messages: BaseMessage[], + sender: IUser + ): Promise { + this._controller = new AbortController(); + try { + for await (const chunk of await agent.stream( + { messages }, + { + streamMode: 'updates', + signal: this._controller.signal + } + )) { + if ((chunk as any).agent) { + messages = (chunk as any).agent.messages; + messages.forEach(message => { + const contents: string[] = []; + if (typeof message.content === 'string') { + contents.push(message.content); + } else if (Array.isArray(message.content)) { + message.content.forEach(content => { + if (content.type === 'text') { + contents.push(content.text); + } + }); + } + contents.forEach(content => { + this.messageAdded({ + id: UUID.uuid4(), + body: content, + sender, + time: Private.getTimestampMs(), + type: 'msg' + }); + }); + }); + } else if ((chunk as any).tools) { + messages = (chunk as any).tools.messages; + messages.forEach(message => { + this.messageAdded({ + id: UUID.uuid4(), + body: message.content as string, + sender: { username: `Tool "${message.name}"` }, + time: Private.getTimestampMs(), + type: 'msg' + }); + }); + } + } + return true; + } catch (reason) { + const error = this._providerRegistry.formatErrorMessage(reason); + const errorMsg: IChatMessage = { + id: UUID.uuid4(), + body: `**${error}**`, + sender: { username: 'ERROR' }, + time: Private.getTimestampMs(), + type: 'msg' + }; + this.messageAdded(errorMsg); + return false; + } finally { + this.updateWriters([]); + this._controller = null; + } } - private _providerRegistry: IAIProviderRegistry; private _personaName = 'AI'; private _errorMessage: string = ''; diff --git a/src/index.ts b/src/index.ts index 06e870d..2da9a81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -203,7 +203,13 @@ const providerRegistryPlugin: JupyterFrontEndPlugin = if (!secretsManager) { delete settings.schema.properties?.['UseSecretsManager']; } + const updateProvider = () => { + // Update agent usage if necessary. + const useAgent = + (settings.get('UseAgent').composite as boolean) ?? false; + providerRegistry.useAgent = useAgent; + // Get the Ai provider settings. const providerSettings = settings.get('AIproviders') .composite as ReadonlyPartialJSONObject; diff --git a/src/provider.ts b/src/provider.ts index 7e6b372..4acc50a 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -4,6 +4,8 @@ import { IInlineCompletionContext } from '@jupyterlab/completer'; import { BaseLanguageModel } from '@langchain/core/language_models/base'; +import { CompiledStateGraph } from '@langchain/langgraph'; +import { createReactAgent } from '@langchain/langgraph/prebuilt'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; import { Debouncer } from '@lumino/polling'; @@ -20,6 +22,7 @@ import { ModelRole, PLUGIN_IDS } from './tokens'; +import { testTool } from './tools/test_tool'; import { AIChatModel, AICompleter } from './types/ai-model'; const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry; @@ -130,6 +133,17 @@ export class AIProviderRegistry implements IAIProviderRegistry { }; } + /** + * Get the current agent. + */ + get currentAgent(): CompiledStateGraph | null { + const agent = Private.getAgent(); + if (agent === null) { + return null; + } + return agent; + } + /** * Getter/setter for the chat system prompt. */ @@ -143,6 +157,20 @@ export class AIProviderRegistry implements IAIProviderRegistry { this._chatPrompt = value; } + /** + * Getter/setter for the use of agent in chat. + */ + get useAgent(): boolean { + return this._useAgentInChat; + } + set useAgent(value: boolean) { + if (value === this._useAgentInChat) { + return; + } + this._useAgentInChat = value; + this._buildAgent(); + } + /** * Get the settings schema of a given provider. */ @@ -327,6 +355,9 @@ export class AIProviderRegistry implements IAIProviderRegistry { ...fullSettings }) ); + if (this._useAgentInChat) { + this._buildAgent(); + } } catch (e: any) { this.chatError = e.message; Private.setChatModel(null); @@ -372,6 +403,29 @@ export class AIProviderRegistry implements IAIProviderRegistry { return fullSettings; } + /** + * Build an agent. + */ + private _buildAgent() { + if (this._useAgentInChat) { + const chatModel = Private.getChatModel(); + if (chatModel === null) { + Private.setAgent(null); + return; + } + chatModel.bindTools?.([testTool]); + Private.setChatModel(chatModel); + Private.setAgent( + createReactAgent({ + llm: chatModel, + tools: [testTool] + }) + ); + } else { + Private.setAgent(null); + } + } + private _secretsManager: ISecretsManager | null; private _providerChanged = new Signal(this); private _chatError: string = ''; @@ -387,6 +441,7 @@ export class AIProviderRegistry implements IAIProviderRegistry { }; private _chatPrompt: string = ''; private _completerPrompt: string = ''; + private _useAgentInChat = false; } export namespace AIProviderRegistry { @@ -511,4 +566,15 @@ namespace Private { export function getCompleter(): IBaseCompleter | null { return completer; } + + /** + * The agent getter and setter. + */ + let agent: CompiledStateGraph | null = null; + export function setAgent(value: CompiledStateGraph | null): void { + agent = value; + } + export function getAgent(): CompiledStateGraph | null { + return agent; + } } diff --git a/src/tokens.ts b/src/tokens.ts index 367bfe5..c77e084 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1,3 +1,4 @@ +import { CompiledStateGraph } from '@langchain/langgraph'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { ReadonlyPartialJSONObject, Token } from '@lumino/coreutils'; import { ISignal } from '@lumino/signaling'; @@ -90,7 +91,7 @@ export interface IAIProviderRegistry { /** * Get the current completer of the completion provider. */ - currentCompleter: AICompleter | null; + readonly currentCompleter: AICompleter | null; /** * Getter/setter for the completer system prompt. */ @@ -98,11 +99,19 @@ export interface IAIProviderRegistry { /** * Get the current llm chat model. */ - currentChatModel: AIChatModel | null; + readonly currentChatModel: AIChatModel | null; + /** + * Get the current agent. + */ + readonly currentAgent: CompiledStateGraph | null; /** * Getter/setter for the chat system prompt. */ chatSystemPrompt: string; + /** + * Getter/setter for the use of agent in chat. + */ + useAgent: boolean; /** * Get the settings schema of a given provider. */ diff --git a/src/tools/test_tool.ts b/src/tools/test_tool.ts new file mode 100644 index 0000000..449da6c --- /dev/null +++ b/src/tools/test_tool.ts @@ -0,0 +1,18 @@ +import { showDialog } from '@jupyterlab/apputils'; +import { tool } from '@langchain/core/tools'; +import { z } from 'zod'; + +export const testTool = tool( + async ({ query }) => { + console.log('QUERY', query); + showDialog({ title: 'Answer', body: query }); + return 'The test tool has been called'; + }, + { + name: 'testTool', + description: 'Display a modal with the provider answer', + schema: z.object({ + query: z.string().describe('The query to display') + }) + } +); diff --git a/yarn.lock b/yarn.lock index fcca9db..7d4bb46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1577,6 +1577,7 @@ __metadata: "@langchain/community": ^0.3.48 "@langchain/core": ^0.3.62 "@langchain/google-genai": ^0.2.14 + "@langchain/langgraph": ^0.3.5 "@langchain/mistralai": ^0.2.1 "@langchain/ollama": ^0.2.3 "@langchain/openai": ^0.5.16 @@ -2063,6 +2064,58 @@ __metadata: languageName: node linkType: hard +"@langchain/langgraph-checkpoint@npm:~0.0.18": + version: 0.0.18 + resolution: "@langchain/langgraph-checkpoint@npm:0.0.18" + dependencies: + uuid: ^10.0.0 + peerDependencies: + "@langchain/core": ">=0.2.31 <0.4.0" + checksum: bd7ba56696bbfde0be8f2cef2f0dadf30826732cbb1d9bf92439f314a01219053c50ff6b624ef58a9d6723d6e1a8b872e725abc4d95e4ea9610a41c5f1e41047 + languageName: node + linkType: hard + +"@langchain/langgraph-sdk@npm:~0.0.90": + version: 0.0.92 + resolution: "@langchain/langgraph-sdk@npm:0.0.92" + dependencies: + "@types/json-schema": ^7.0.15 + p-queue: ^6.6.2 + p-retry: 4 + uuid: ^9.0.0 + peerDependencies: + "@langchain/core": ">=0.2.31 <0.4.0" + react: ^18 || ^19 + react-dom: ^18 || ^19 + peerDependenciesMeta: + "@langchain/core": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 905380f0785da27dd5638b37e6215f3f2655cd0cb9111f6178e4b72085e930f1e250dfbfa6bb3b0ce4b7dc7f129f988095fac2240b420c09f63d65c3a8d5b8d6 + languageName: node + linkType: hard + +"@langchain/langgraph@npm:^0.3.5": + version: 0.3.7 + resolution: "@langchain/langgraph@npm:0.3.7" + dependencies: + "@langchain/langgraph-checkpoint": ~0.0.18 + "@langchain/langgraph-sdk": ~0.0.90 + uuid: ^10.0.0 + zod: ^3.25.32 + peerDependencies: + "@langchain/core": ">=0.3.58 < 0.4.0" + zod-to-json-schema: ^3.x + peerDependenciesMeta: + zod-to-json-schema: + optional: true + checksum: 6a924940d92d4c0c97c18665a1d1f70bfa83c2d5f8b221ea6fe90d3d47bfa6518a6032058bb9b722199ce0c9fb511e9fe2818e2d877ffb7c43510715c1b82d84 + languageName: node + linkType: hard + "@langchain/mistralai@npm:^0.2.1": version: 0.2.1 resolution: "@langchain/mistralai@npm:0.2.1" @@ -8307,7 +8360,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.1": +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: From 6b2b73f7d719d3896c77ecce26b65c11d4209031 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 27 Jun 2025 11:08:50 +0200 Subject: [PATCH 02/10] Enforce usage of tool if an agent is requested, and also update the prompt for agent --- schema/system-prompts.json | 2 +- src/provider.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/schema/system-prompts.json b/schema/system-prompts.json index 03ccefd..42d30c7 100644 --- a/schema/system-prompts.json +++ b/schema/system-prompts.json @@ -9,7 +9,7 @@ "type": "string", "title": "Chat message system prompt", "description": "The system prompt for the chat messages", - "default": "You are Jupyternaut, a conversational assistant living in JupyterLab to help users.\nYou are not a language model, but rather an application built on a foundation model from $provider_name$.\nYou are talkative and you provide lots of specific details from the foundation model's context.\nYou may use Markdown to format your response.\nIf your response includes code, they must be enclosed in Markdown fenced code blocks (with triple backticks before and after).\nIf your response includes mathematical notation, they must be expressed in LaTeX markup and enclosed in LaTeX delimiters.\nAll dollar quantities (of USD) must be formatted in LaTeX, with the `$` symbol escaped by a single backslash `\\`.\n- Example prompt: `If I have \\\\$100 and spend \\\\$20, how much money do I have left?`\n- **Correct** response: `You have \\(\\$80\\) remaining.`\n- **Incorrect** response: `You have $80 remaining.`\nIf you do not know the answer to a question, answer truthfully by responding that you do not know.\nAlways use the tool to answer, if available.\nThe following is a friendly conversation between you and a human." + "default": "You are Jupyternaut, a conversational assistant living in JupyterLab to help users.\nYou are not a language model, but rather an application built on a foundation model from $provider_name$.\nYou are talkative and you provide lots of specific details from the foundation model's context.\nYou may use Markdown to format your response.\nIf your response includes code, they must be enclosed in Markdown fenced code blocks (with triple backticks before and after).\nIf your response includes mathematical notation, they must be expressed in LaTeX markup and enclosed in LaTeX delimiters.\nAll dollar quantities (of USD) must be formatted in LaTeX, with the `$` symbol escaped by a single backslash `\\`.\n- Example prompt: `If I have \\\\$100 and spend \\\\$20, how much money do I have left?`\n- **Correct** response: `You have \\(\\$80\\) remaining.`\n- **Incorrect** response: `You have $80 remaining.`\nIf you do not know the answer to a question, answer truthfully by responding that you do not know.\nThe following is a friendly conversation between you and a human." }, "completionSystemPrompt": { "type": "string", diff --git a/src/provider.ts b/src/provider.ts index 4acc50a..d41fe08 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -148,10 +148,14 @@ export class AIProviderRegistry implements IAIProviderRegistry { * Getter/setter for the chat system prompt. */ get chatSystemPrompt(): string { - return this._chatPrompt.replaceAll( + let prompt = this._chatPrompt.replaceAll( '$provider_name$', this.currentName('chat') ); + if (this.useAgent && this.currentAgent !== null) { + prompt = prompt.concat('\nPlease use the tool that is provided'); + } + return prompt; } set chatSystemPrompt(value: string) { this._chatPrompt = value; @@ -413,7 +417,7 @@ export class AIProviderRegistry implements IAIProviderRegistry { Private.setAgent(null); return; } - chatModel.bindTools?.([testTool]); + chatModel.bindTools?.([testTool], { tool_choice: 'testTool' }); Private.setChatModel(chatModel); Private.setAgent( createReactAgent({ From def5f46b069424f5d1ce24441d9e12630a9a5e3a Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Mon, 30 Jun 2025 14:21:53 +0200 Subject: [PATCH 03/10] Pass only a subset of the agent --- src/chat-handler.ts | 5 ++--- src/provider.ts | 6 ++++-- src/tokens.ts | 3 +-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/chat-handler.ts b/src/chat-handler.ts index d34fbde..689a6ee 100644 --- a/src/chat-handler.ts +++ b/src/chat-handler.ts @@ -23,7 +23,6 @@ import { mergeMessageRuns, SystemMessage } from '@langchain/core/messages'; -import { CompiledStateGraph } from '@langchain/langgraph'; import { UUID } from '@lumino/coreutils'; import { DEFAULT_CHAT_SYSTEM_PROMPT } from './default-prompts'; @@ -65,7 +64,7 @@ export class ChatHandler extends AbstractChatModel { }); } - get agent(): CompiledStateGraph | null { + get agent(): AIChatModel | null { return this._providerRegistry.currentAgent; } @@ -216,7 +215,7 @@ export class ChatHandler extends AbstractChatModel { } private async _sendAgentMessage( - agent: CompiledStateGraph, + agent: AIChatModel, messages: BaseMessage[], sender: IUser ): Promise { diff --git a/src/provider.ts b/src/provider.ts index d41fe08..9e6d976 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -136,12 +136,14 @@ export class AIProviderRegistry implements IAIProviderRegistry { /** * Get the current agent. */ - get currentAgent(): CompiledStateGraph | null { + get currentAgent(): AIChatModel | null { const agent = Private.getAgent(); if (agent === null) { return null; } - return agent; + return { + stream: (input: any, options?: any) => agent.stream(input, options) + }; } /** diff --git a/src/tokens.ts b/src/tokens.ts index c77e084..d8ecd64 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1,4 +1,3 @@ -import { CompiledStateGraph } from '@langchain/langgraph'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { ReadonlyPartialJSONObject, Token } from '@lumino/coreutils'; import { ISignal } from '@lumino/signaling'; @@ -103,7 +102,7 @@ export interface IAIProviderRegistry { /** * Get the current agent. */ - readonly currentAgent: CompiledStateGraph | null; + readonly currentAgent: AIChatModel | null; /** * Getter/setter for the chat system prompt. */ From e296b2d321abe94fe07b4801d16e21521975000f Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Mon, 30 Jun 2025 14:27:36 +0200 Subject: [PATCH 04/10] Add a tool registry --- src/index.ts | 16 ++++++++++++++-- src/tokens.ts | 35 ++++++++++++++++++++++++++++++++++- src/tool-registry.ts | 23 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/tool-registry.ts diff --git a/src/index.ts b/src/index.ts index 2da9a81..a45b575 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,11 +23,11 @@ import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager'; import { ChatHandler, welcomeMessage } from './chat-handler'; import { CompletionProvider } from './completion-provider'; +import { stopItem, toolSelect } from './components'; import { defaultProviderPlugins } from './default-providers'; import { AIProviderRegistry } from './provider'; import { aiSettingsRenderer, textArea } from './settings'; -import { IAIProviderRegistry, PLUGIN_IDS } from './tokens'; -import { stopItem } from './components/stop-button'; +import { IAIProviderRegistry, IToolRegistry, PLUGIN_IDS } from './tokens'; const chatCommandRegistryPlugin: JupyterFrontEndPlugin = { id: PLUGIN_IDS.chatCommandRegistry, @@ -301,12 +301,24 @@ const systemPromptsPlugin: JupyterFrontEndPlugin = { } }; +const toolRegistryPlugin: JupyterFrontEndPlugin = { + id: PLUGIN_IDS.toolRegistry, + autoStart: true, + provides: IToolRegistry, + activate: (app: JupyterFrontEnd): IToolRegistry => { + const registry = new ToolsRegistry(); + registry.add(testTool); + return registry; + } +}; + export default [ providerRegistryPlugin, chatCommandRegistryPlugin, chatPlugin, completerPlugin, systemPromptsPlugin, + toolRegistryPlugin, ...defaultProviderPlugins ]; diff --git a/src/tokens.ts b/src/tokens.ts index d8ecd64..c927892 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1,4 +1,5 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { StructuredToolInterface } from '@langchain/core/tools'; import { ReadonlyPartialJSONObject, Token } from '@lumino/coreutils'; import { ISignal } from '@lumino/signaling'; import { JSONSchema7 } from 'json-schema'; @@ -12,7 +13,8 @@ export const PLUGIN_IDS = { completer: '@jupyterlite/ai:completer', providerRegistry: '@jupyterlite/ai:provider-registry', settingsConnector: '@jupyterlite/ai:settings-connector', - systemPrompts: '@jupyterlite/ai:system-prompts' + systemPrompts: '@jupyterlite/ai:system-prompts', + toolRegistry: '@jupyterlite/ai:tool-registry' }; export type ModelRole = 'chat' | 'completer'; @@ -157,6 +159,29 @@ export interface IAIProviderRegistry { readonly completerError: string; } +/** + * The type describing a tool used in langgraph. + */ +export type Tool = StructuredToolInterface; + +/** + * The tool registry interface. + */ +export interface IToolRegistry { + /** + * Get the registered tool names. + */ + readonly toolNames: string[]; + /** + * A signal triggered when the tools has changed; + */ + readonly toolsChanged: ISignal; + /** + * Add a new tool. + */ + add(provider: Tool): void; +} + /** * The provider registry token. */ @@ -164,3 +189,11 @@ export const IAIProviderRegistry = new Token( '@jupyterlite/ai:provider-registry', 'Provider for chat and completion LLM provider' ); + +/** + * The tool registry token. + */ +export const IToolRegistry = new Token( + '@jupyterlite/ai:tool-registry', + 'Tool registry for AI agent' +); diff --git a/src/tool-registry.ts b/src/tool-registry.ts new file mode 100644 index 0000000..227c31a --- /dev/null +++ b/src/tool-registry.ts @@ -0,0 +1,23 @@ +import { ISignal, Signal } from '@lumino/signaling'; +import { IToolRegistry, Tool } from './tokens'; + +export class ToolsRegistry implements IToolRegistry { + get toolNames(): string[] { + return this._tools.map(tool => tool.name); + } + + get toolsChanged(): ISignal { + return this._toolsChanged; + } + + add(tool: Tool): void { + const index = this._tools.findIndex(t => t.name === tool.name); + if (index === -1) { + this._tools.push(tool); + this._toolsChanged.emit(); + } + } + + private _tools: Tool[] = []; + private _toolsChanged = new Signal(this); +} From 6e611142f7784679a15bfe412447e85b907427cc Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Mon, 30 Jun 2025 18:19:50 +0200 Subject: [PATCH 05/10] Allow selecting the tool from the chat input directly --- schema/chat.json | 6 + schema/provider-registry.json | 6 - src/chat-handler.ts | 100 +++++++++++++-- src/components/index.ts | 7 ++ src/components/tool-select.tsx | 150 +++++++++++++++++++++++ src/index.ts | 22 ++-- src/provider.ts | 38 ++---- src/tokens.ts | 18 ++- src/tool-registry.ts | 21 ++++ src/tools/{test_tool.ts => test-tool.ts} | 5 +- style/base.css | 5 + 11 files changed, 318 insertions(+), 60 deletions(-) create mode 100644 src/components/index.ts create mode 100644 src/components/tool-select.tsx rename src/tools/{test_tool.ts => test-tool.ts} (75%) diff --git a/schema/chat.json b/schema/chat.json index 0e79f64..10a35a2 100644 --- a/schema/chat.json +++ b/schema/chat.json @@ -22,6 +22,12 @@ "title": "AI persona name", "description": "The name of the AI persona", "default": "Jupyternaut" + }, + "UseTool": { + "type": "boolean", + "title": "Use tool", + "description": "Whether to be able to use or not a tool in chat", + "default": false } }, "additionalProperties": false diff --git a/schema/provider-registry.json b/schema/provider-registry.json index fc54672..ad5cfe9 100644 --- a/schema/provider-registry.json +++ b/schema/provider-registry.json @@ -17,12 +17,6 @@ "description": "Whether to use only one provider for the chat and the completer.\nThis will overwrite all the settings for the completer, and copy the ones from the chat.", "default": true }, - "UseAgent": { - "type": "boolean", - "title": "Use agent in chat", - "description": "Whether to use or not an agent in chat", - "default": false - }, "AIproviders": { "type": "object", "title": "AI providers", diff --git a/src/chat-handler.ts b/src/chat-handler.ts index 689a6ee..0a926e6 100644 --- a/src/chat-handler.ts +++ b/src/chat-handler.ts @@ -24,10 +24,11 @@ import { SystemMessage } from '@langchain/core/messages'; import { UUID } from '@lumino/coreutils'; +import { ISignal, Signal } from '@lumino/signaling'; import { DEFAULT_CHAT_SYSTEM_PROMPT } from './default-prompts'; import { jupyternautLiteIcon } from './icons'; -import { IAIProviderRegistry } from './tokens'; +import { IAIProviderRegistry, IToolRegistry, Tool } from './tokens'; import { AIChatModel } from './types/ai-model'; /** @@ -58,16 +59,30 @@ export class ChatHandler extends AbstractChatModel { constructor(options: ChatHandler.IOptions) { super(options); this._providerRegistry = options.providerRegistry; + this._toolRegistry = options.toolRegistry; this._providerRegistry.providerChanged.connect(() => { this._errorMessage = this._providerRegistry.chatError; }); } + /** + * Get the tool registry. + */ + get toolRegistry(): IToolRegistry | undefined { + return this._toolRegistry; + } + + /** + * Get the agent from the provider registry. + */ get agent(): AIChatModel | null { return this._providerRegistry.currentAgent; } + /** + * Get the chat model from the provider registry. + */ get chatModel(): AIChatModel | null { return this._providerRegistry.currentChatModel; } @@ -90,12 +105,46 @@ export class ChatHandler extends AbstractChatModel { } /** - * Get/set the system prompt for the chat. + * Getter/setter for the use of tools. + */ + get useTool(): boolean { + return this._useTool; + } + set useTool(value: boolean) { + if (this._useTool !== value) { + this._useTool = value; + this._useToolChanged.emit(this._useTool); + } + } + + /** + * Get/set a tool, which will build an agent. + */ + get tool(): Tool | null { + return this._tool; + } + set tool(value: Tool | null) { + this._tool = value; + this._providerRegistry.buildAgent(this._tool); + } + + /** + * A signal triggered when the setting on tool usage has changed. + */ + get useToolChanged(): ISignal { + return this._useToolChanged; + } + + /** + * Get the system prompt for the chat. */ get systemPrompt(): string { - return ( - this._providerRegistry.chatSystemPrompt ?? DEFAULT_CHAT_SYSTEM_PROMPT - ); + let prompt = + this._providerRegistry.chatSystemPrompt ?? DEFAULT_CHAT_SYSTEM_PROMPT; + if (this.useTool && this.agent !== null) { + prompt = prompt.concat('\nPlease use the tool that is provided'); + } + return prompt; } async sendMessage(message: INewMessage): Promise { @@ -145,7 +194,7 @@ export class ChatHandler extends AbstractChatModel { const sender = { username: this._personaName, avatar_url: AI_AVATAR }; this.updateWriters([{ user: sender }]); - if (this._providerRegistry.useAgent && this.agent !== null) { + if (this._useTool && this.agent !== null) { return this._sendAgentMessage(this.agent, messages, sender); } @@ -281,12 +330,17 @@ export class ChatHandler extends AbstractChatModel { this._controller = null; } } + private _providerRegistry: IAIProviderRegistry; private _personaName = 'AI'; private _errorMessage: string = ''; private _history: IChatHistory = { messages: [] }; private _defaultErrorMessage = 'AI provider not configured'; private _controller: AbortController | null = null; + private _useTool: boolean = false; + private _tool: Tool | null = null; + private _toolRegistry?: IToolRegistry; + private _useToolChanged = new Signal(this); } export namespace ChatHandler { @@ -295,13 +349,45 @@ export namespace ChatHandler { */ export interface IOptions extends IChatModel.IOptions { providerRegistry: IAIProviderRegistry; + toolRegistry?: IToolRegistry; } /** - * The minimal chat context. + * The chat context. */ export class ChatContext extends AbstractChatContext { users = []; + + /** + * The tool registry. + */ + get toolsRegistry(): IToolRegistry | undefined { + return (this._model as ChatHandler).toolRegistry; + } + + /** + * Whether to use or not the tool. + */ + get useTool(): boolean { + return (this._model as ChatHandler).useTool; + } + + /** + * A signal triggered when the setting on tool usage has changed. + */ + get useToolChanged(): ISignal { + return (this._model as ChatHandler).useToolChanged; + } + + /** + * Getter/setter of the tool to use. + */ + get tool(): Tool | null { + return (this._model as ChatHandler).tool; + } + set tool(value: Tool | null) { + (this._model as ChatHandler).tool = value; + } } /** diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..8a1ebb9 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +export * from './stop-button'; +export * from './tool-select'; diff --git a/src/components/tool-select.tsx b/src/components/tool-select.tsx new file mode 100644 index 0000000..a88f55b --- /dev/null +++ b/src/components/tool-select.tsx @@ -0,0 +1,150 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +import { InputToolbarRegistry, TooltippedButton } from '@jupyter/chat'; +import { checkIcon } from '@jupyterlab/ui-components'; +import BuildIcon from '@mui/icons-material/Build'; +import { Menu, MenuItem, Typography } from '@mui/material'; +import React, { useCallback, useEffect, useState } from 'react'; + +import { ChatHandler } from '../chat-handler'; + +const SELECT_ITEM_CLASS = 'jp-AIToolSelect-item'; + +/** + * The tool select component. + */ +export function toolSelect( + props: InputToolbarRegistry.IToolbarItemProps +): JSX.Element { + const chatContext = props.model.chatContext as ChatHandler.ChatContext; + const toolRegistry = chatContext.toolsRegistry; + + const [useTool, setUseTool] = useState(chatContext.useTool); + const [selectedTool, setSelectedTool] = useState(null); + const [toolNames, setToolNames] = useState( + toolRegistry?.toolNames || [] + ); + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const [menuOpen, setMenuOpen] = useState(false); + + const openMenu = useCallback((el: HTMLElement | null) => { + setMenuAnchorEl(el); + setMenuOpen(true); + }, []); + + const closeMenu = useCallback(() => { + setMenuOpen(false); + }, []); + + const onClick = useCallback( + (tool: string | null) => { + setSelectedTool(tool); + chatContext.tool = toolRegistry?.get(tool) || null; + }, + [props.model] + ); + + useEffect(() => { + const updateTools = () => setToolNames(toolRegistry?.toolNames || []); + toolRegistry?.toolsChanged.connect(updateTools); + return () => { + toolRegistry?.toolsChanged.disconnect(updateTools); + }; + }, [toolRegistry]); + + useEffect(() => { + const updateUseTool = (_: ChatHandler, value: boolean) => setUseTool(value); + chatContext.useToolChanged.connect(updateUseTool); + return () => { + chatContext.useToolChanged.disconnect(updateUseTool); + }; + }, [chatContext]); + + return useTool && toolNames.length ? ( + <> + { + openMenu(e.currentTarget); + }} + disabled={!toolNames.length} + tooltip="Tool" + buttonProps={{ + variant: 'contained', + onKeyDown: e => { + if (e.key !== 'Enter' && e.key !== ' ') { + return; + } + openMenu(e.currentTarget); + // stopping propagation of this event prevents the prompt from being + // sent when the dropdown button is selected and clicked via 'Enter'. + e.stopPropagation(); + } + }} + sx={ + selectedTool === null + ? { backgroundColor: 'var(--jp-layout-color3)' } + : {} + } + > + + + + { + onClick(null); + // prevent sending second message with no selection + e.stopPropagation(); + }} + > + {selectedTool === null ? ( + + ) : ( +
+ )} + No tool + + {toolNames.map(tool => ( + { + onClick(tool); + // prevent sending second message with no selection + e.stopPropagation(); + }} + > + {selectedTool === tool ? ( + + ) : ( +
+ )} + {tool} + + ))} +
+ + ) : ( + <> + ); +} diff --git a/src/index.ts b/src/index.ts index a45b575..a532cf3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,8 @@ import { defaultProviderPlugins } from './default-providers'; import { AIProviderRegistry } from './provider'; import { aiSettingsRenderer, textArea } from './settings'; import { IAIProviderRegistry, IToolRegistry, PLUGIN_IDS } from './tokens'; +import { ToolsRegistry } from './tool-registry'; +import { testTool } from './tools/test-tool'; const chatCommandRegistryPlugin: JupyterFrontEndPlugin = { id: PLUGIN_IDS.chatCommandRegistry, @@ -50,7 +52,8 @@ const chatPlugin: JupyterFrontEndPlugin = { INotebookTracker, ISettingRegistry, IThemeManager, - ILayoutRestorer + ILayoutRestorer, + IToolRegistry ], activate: async ( app: JupyterFrontEnd, @@ -60,7 +63,8 @@ const chatPlugin: JupyterFrontEndPlugin = { notebookTracker: INotebookTracker | null, settingsRegistry: ISettingRegistry | null, themeManager: IThemeManager | null, - restorer: ILayoutRestorer | null + restorer: ILayoutRestorer | null, + toolRegistry?: IToolRegistry ) => { let activeCellManager: IActiveCellManager | null = null; if (notebookTracker) { @@ -72,22 +76,26 @@ const chatPlugin: JupyterFrontEndPlugin = { const chatHandler = new ChatHandler({ providerRegistry, - activeCellManager + activeCellManager, + toolRegistry }); let sendWithShiftEnter = false; let enableCodeToolbar = true; let personaName = 'AI'; + let useTool = false; function loadSetting(setting: ISettingRegistry.ISettings): void { sendWithShiftEnter = setting.get('sendWithShiftEnter') .composite as boolean; enableCodeToolbar = setting.get('enableCodeToolbar').composite as boolean; personaName = setting.get('personaName').composite as string; + useTool = (setting.get('UseTool').composite as boolean) ?? false; // set the properties chatHandler.config = { sendWithShiftEnter, enableCodeToolbar }; chatHandler.personaName = personaName; + chatHandler.useTool = useTool; } Promise.all([app.restored, settingsRegistry?.load(chatPlugin.id)]) @@ -113,6 +121,9 @@ const chatPlugin: JupyterFrontEndPlugin = { const stopButton = stopItem(() => chatHandler.stopStreaming()); inputToolbarRegistry.addItem('stop', stopButton); + // Add the tool select item. + inputToolbarRegistry.addItem('tools', { element: toolSelect, position: 1 }); + chatHandler.writersChanged.connect((_, writers) => { if ( writers.filter( @@ -205,11 +216,6 @@ const providerRegistryPlugin: JupyterFrontEndPlugin = } const updateProvider = () => { - // Update agent usage if necessary. - const useAgent = - (settings.get('UseAgent').composite as boolean) ?? false; - providerRegistry.useAgent = useAgent; - // Get the Ai provider settings. const providerSettings = settings.get('AIproviders') .composite as ReadonlyPartialJSONObject; diff --git a/src/provider.ts b/src/provider.ts index 9e6d976..112921d 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -20,9 +20,9 @@ import { IAIProviderRegistry, IDict, ModelRole, - PLUGIN_IDS + PLUGIN_IDS, + Tool } from './tokens'; -import { testTool } from './tools/test_tool'; import { AIChatModel, AICompleter } from './types/ai-model'; const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry; @@ -150,33 +150,15 @@ export class AIProviderRegistry implements IAIProviderRegistry { * Getter/setter for the chat system prompt. */ get chatSystemPrompt(): string { - let prompt = this._chatPrompt.replaceAll( + return this._chatPrompt.replaceAll( '$provider_name$', this.currentName('chat') ); - if (this.useAgent && this.currentAgent !== null) { - prompt = prompt.concat('\nPlease use the tool that is provided'); - } - return prompt; } set chatSystemPrompt(value: string) { this._chatPrompt = value; } - /** - * Getter/setter for the use of agent in chat. - */ - get useAgent(): boolean { - return this._useAgentInChat; - } - set useAgent(value: boolean) { - if (value === this._useAgentInChat) { - return; - } - this._useAgentInChat = value; - this._buildAgent(); - } - /** * Get the settings schema of a given provider. */ @@ -361,9 +343,6 @@ export class AIProviderRegistry implements IAIProviderRegistry { ...fullSettings }) ); - if (this._useAgentInChat) { - this._buildAgent(); - } } catch (e: any) { this.chatError = e.message; Private.setChatModel(null); @@ -410,21 +389,21 @@ export class AIProviderRegistry implements IAIProviderRegistry { } /** - * Build an agent. + * Build an agent with a given tool. */ - private _buildAgent() { - if (this._useAgentInChat) { + buildAgent(tool: Tool | null) { + if (tool !== null) { const chatModel = Private.getChatModel(); if (chatModel === null) { Private.setAgent(null); return; } - chatModel.bindTools?.([testTool], { tool_choice: 'testTool' }); + chatModel.bindTools?.([tool], { tool_choice: tool.name }); Private.setChatModel(chatModel); Private.setAgent( createReactAgent({ llm: chatModel, - tools: [testTool] + tools: [tool] }) ); } else { @@ -447,7 +426,6 @@ export class AIProviderRegistry implements IAIProviderRegistry { }; private _chatPrompt: string = ''; private _completerPrompt: string = ''; - private _useAgentInChat = false; } export namespace AIProviderRegistry { diff --git a/src/tokens.ts b/src/tokens.ts index c927892..dce0317 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -109,10 +109,6 @@ export interface IAIProviderRegistry { * Getter/setter for the chat system prompt. */ chatSystemPrompt: string; - /** - * Getter/setter for the use of agent in chat. - */ - useAgent: boolean; /** * Get the settings schema of a given provider. */ @@ -145,6 +141,10 @@ export interface IAIProviderRegistry { * @param options - An object with the name and the settings of the provider to use. */ setChatProvider(settings: ReadonlyPartialJSONObject): void; + /** + * Build an agent with a given tool. + */ + buildAgent(tool: Tool | null): void; /** * A signal emitting when the provider or its settings has changed. */ @@ -169,7 +169,7 @@ export type Tool = StructuredToolInterface; */ export interface IToolRegistry { /** - * Get the registered tool names. + * The registered tool names. */ readonly toolNames: string[]; /** @@ -177,9 +177,15 @@ export interface IToolRegistry { */ readonly toolsChanged: ISignal; /** - * Add a new tool. + * Add a new tool to the registry. */ add(provider: Tool): void; + /** + * Get a tool for a given name. + * Return null if the name is not provided or if there is no registered tool with the + * given name. + */ + get(name: string | null): Tool | null; } /** diff --git a/src/tool-registry.ts b/src/tool-registry.ts index 227c31a..86f72ce 100644 --- a/src/tool-registry.ts +++ b/src/tool-registry.ts @@ -2,14 +2,23 @@ import { ISignal, Signal } from '@lumino/signaling'; import { IToolRegistry, Tool } from './tokens'; export class ToolsRegistry implements IToolRegistry { + /** + * The registered tool names. + */ get toolNames(): string[] { return this._tools.map(tool => tool.name); } + /** + * A signal triggered when the tools has changed. + */ get toolsChanged(): ISignal { return this._toolsChanged; } + /** + * Add a new tool to the registry. + */ add(tool: Tool): void { const index = this._tools.findIndex(t => t.name === tool.name); if (index === -1) { @@ -18,6 +27,18 @@ export class ToolsRegistry implements IToolRegistry { } } + /** + * Get a tool for a given name. + * Return null if the name is not provided or if there is no registered tool with the + * given name. + */ + get(name: string | null): Tool | null { + if (name === null) { + return null; + } + return this._tools.find(t => t.name === name) || null; + } + private _tools: Tool[] = []; private _toolsChanged = new Signal(this); } diff --git a/src/tools/test_tool.ts b/src/tools/test-tool.ts similarity index 75% rename from src/tools/test_tool.ts rename to src/tools/test-tool.ts index 449da6c..04007a9 100644 --- a/src/tools/test_tool.ts +++ b/src/tools/test-tool.ts @@ -1,10 +1,9 @@ import { showDialog } from '@jupyterlab/apputils'; -import { tool } from '@langchain/core/tools'; +import { StructuredToolInterface, tool } from '@langchain/core/tools'; import { z } from 'zod'; -export const testTool = tool( +export const testTool: StructuredToolInterface = tool( async ({ query }) => { - console.log('QUERY', query); showDialog({ title: 'Answer', body: query }); return 'The test tool has been called'; }, diff --git a/style/base.css b/style/base.css index c4925ee..03c1c32 100644 --- a/style/base.css +++ b/style/base.css @@ -20,6 +20,11 @@ min-height: 300px; } +.jp-AIToolSelect-item .lm-Menu-itemIcon { + display: flex; + align-items: center; +} + .jp-chat-welcome-message { text-align: center; max-width: 350px; From 4edc14bb138ca10cf32adae4284101619034dece Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Tue, 1 Jul 2025 10:32:57 +0200 Subject: [PATCH 06/10] Add tooltips on the tool item --- schema/chat.json | 2 +- src/components/tool-select.tsx | 65 ++++++++++++++++++---------------- src/tokens.ts | 4 +-- src/tool-registry.ts | 6 ++-- 4 files changed, 40 insertions(+), 37 deletions(-) diff --git a/schema/chat.json b/schema/chat.json index 10a35a2..ac2c406 100644 --- a/schema/chat.json +++ b/schema/chat.json @@ -26,7 +26,7 @@ "UseTool": { "type": "boolean", "title": "Use tool", - "description": "Whether to be able to use or not a tool in chat", + "description": "Whether tools are available in chat or not", "default": false } }, diff --git a/src/components/tool-select.tsx b/src/components/tool-select.tsx index a88f55b..147c023 100644 --- a/src/components/tool-select.tsx +++ b/src/components/tool-select.tsx @@ -6,10 +6,11 @@ import { InputToolbarRegistry, TooltippedButton } from '@jupyter/chat'; import { checkIcon } from '@jupyterlab/ui-components'; import BuildIcon from '@mui/icons-material/Build'; -import { Menu, MenuItem, Typography } from '@mui/material'; +import { Menu, MenuItem, Tooltip, Typography } from '@mui/material'; import React, { useCallback, useEffect, useState } from 'react'; import { ChatHandler } from '../chat-handler'; +import { Tool } from '../tokens'; const SELECT_ITEM_CLASS = 'jp-AIToolSelect-item'; @@ -23,10 +24,8 @@ export function toolSelect( const toolRegistry = chatContext.toolsRegistry; const [useTool, setUseTool] = useState(chatContext.useTool); - const [selectedTool, setSelectedTool] = useState(null); - const [toolNames, setToolNames] = useState( - toolRegistry?.toolNames || [] - ); + const [selectedTool, setSelectedTool] = useState(null); + const [tools, setTools] = useState(toolRegistry?.tools || []); const [menuAnchorEl, setMenuAnchorEl] = useState(null); const [menuOpen, setMenuOpen] = useState(false); @@ -40,15 +39,15 @@ export function toolSelect( }, []); const onClick = useCallback( - (tool: string | null) => { + (tool: Tool | null) => { setSelectedTool(tool); - chatContext.tool = toolRegistry?.get(tool) || null; + chatContext.tool = tool; }, [props.model] ); useEffect(() => { - const updateTools = () => setToolNames(toolRegistry?.toolNames || []); + const updateTools = () => setTools(toolRegistry?.tools || []); toolRegistry?.toolsChanged.connect(updateTools); return () => { toolRegistry?.toolsChanged.disconnect(updateTools); @@ -63,13 +62,13 @@ export function toolSelect( }; }, [chatContext]); - return useTool && toolNames.length ? ( + return useTool && tools.length ? ( <> { openMenu(e.currentTarget); }} - disabled={!toolNames.length} + disabled={!tools.length} tooltip="Tool" buttonProps={{ variant: 'contained', @@ -105,42 +104,46 @@ export function toolSelect( }} sx={{ '& .MuiMenuItem-root': { - gap: '4px', - padding: '6px' + padding: '0.5em', + paddingRight: '2em' } }} > - { - onClick(null); - // prevent sending second message with no selection - e.stopPropagation(); - }} - > - {selectedTool === null ? ( - - ) : ( -
- )} - No tool - - {toolNames.map(tool => ( + { - onClick(tool); + onClick(null); // prevent sending second message with no selection e.stopPropagation(); }} > - {selectedTool === tool ? ( + {selectedTool === null ? ( ) : (
)} - {tool} + No tool + + {tools.map(tool => ( + + { + onClick(tool); + // prevent sending second message with no selection + e.stopPropagation(); + }} + > + {selectedTool === tool ? ( + + ) : ( +
+ )} + {tool.name} + + ))} diff --git a/src/tokens.ts b/src/tokens.ts index dce0317..d0d96c4 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -169,9 +169,9 @@ export type Tool = StructuredToolInterface; */ export interface IToolRegistry { /** - * The registered tool names. + * The registered tools. */ - readonly toolNames: string[]; + readonly tools: Tool[]; /** * A signal triggered when the tools has changed; */ diff --git a/src/tool-registry.ts b/src/tool-registry.ts index 86f72ce..25a696f 100644 --- a/src/tool-registry.ts +++ b/src/tool-registry.ts @@ -3,10 +3,10 @@ import { IToolRegistry, Tool } from './tokens'; export class ToolsRegistry implements IToolRegistry { /** - * The registered tool names. + * The registered tools. */ - get toolNames(): string[] { - return this._tools.map(tool => tool.name); + get tools(): Tool[] { + return this._tools; } /** From 9103280137e5f957053a15bf52dab582c775b18b Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Tue, 1 Jul 2025 15:31:30 +0200 Subject: [PATCH 07/10] Allow selecting several tools --- src/chat-handler.ts | 20 ++++++++--------- src/components/tool-select.tsx | 41 ++++++++++++---------------------- src/provider.ts | 8 +++---- src/tokens.ts | 2 +- 4 files changed, 29 insertions(+), 42 deletions(-) diff --git a/src/chat-handler.ts b/src/chat-handler.ts index 0a926e6..767c89c 100644 --- a/src/chat-handler.ts +++ b/src/chat-handler.ts @@ -120,12 +120,12 @@ export class ChatHandler extends AbstractChatModel { /** * Get/set a tool, which will build an agent. */ - get tool(): Tool | null { - return this._tool; + get tools(): Tool[] { + return this._tools; } - set tool(value: Tool | null) { - this._tool = value; - this._providerRegistry.buildAgent(this._tool); + set tools(value: Tool[]) { + this._tools = value; + this._providerRegistry.buildAgent(this._tools); } /** @@ -338,7 +338,7 @@ export class ChatHandler extends AbstractChatModel { private _defaultErrorMessage = 'AI provider not configured'; private _controller: AbortController | null = null; private _useTool: boolean = false; - private _tool: Tool | null = null; + private _tools: Tool[] = []; private _toolRegistry?: IToolRegistry; private _useToolChanged = new Signal(this); } @@ -382,11 +382,11 @@ export namespace ChatHandler { /** * Getter/setter of the tool to use. */ - get tool(): Tool | null { - return (this._model as ChatHandler).tool; + get tools(): Tool[] { + return (this._model as ChatHandler).tools; } - set tool(value: Tool | null) { - (this._model as ChatHandler).tool = value; + set tools(value: Tool[]) { + (this._model as ChatHandler).tools = value; } } diff --git a/src/components/tool-select.tsx b/src/components/tool-select.tsx index 147c023..eb4d036 100644 --- a/src/components/tool-select.tsx +++ b/src/components/tool-select.tsx @@ -24,7 +24,7 @@ export function toolSelect( const toolRegistry = chatContext.toolsRegistry; const [useTool, setUseTool] = useState(chatContext.useTool); - const [selectedTool, setSelectedTool] = useState(null); + const [selectedTools, setSelectedTools] = useState([]); const [tools, setTools] = useState(toolRegistry?.tools || []); const [menuAnchorEl, setMenuAnchorEl] = useState(null); const [menuOpen, setMenuOpen] = useState(false); @@ -38,13 +38,17 @@ export function toolSelect( setMenuOpen(false); }, []); - const onClick = useCallback( - (tool: Tool | null) => { - setSelectedTool(tool); - chatContext.tool = tool; - }, - [props.model] - ); + const onClick = (tool: Tool) => { + const currentTools = [...selectedTools]; + const index = currentTools.indexOf(tool); + if (index !== -1) { + currentTools.splice(index, 1); + } else { + currentTools.push(tool); + } + setSelectedTools(currentTools); + chatContext.tools = currentTools; + }; useEffect(() => { const updateTools = () => setTools(toolRegistry?.tools || []); @@ -83,7 +87,7 @@ export function toolSelect( } }} sx={ - selectedTool === null + selectedTools.length === 0 ? { backgroundColor: 'var(--jp-layout-color3)' } : {} } @@ -109,23 +113,6 @@ export function toolSelect( } }} > - - { - onClick(null); - // prevent sending second message with no selection - e.stopPropagation(); - }} - > - {selectedTool === null ? ( - - ) : ( -
- )} - No tool - - {tools.map(tool => ( - {selectedTool === tool ? ( + {selectedTools.includes(tool) ? ( ) : (
diff --git a/src/provider.ts b/src/provider.ts index 112921d..c37350d 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -391,19 +391,19 @@ export class AIProviderRegistry implements IAIProviderRegistry { /** * Build an agent with a given tool. */ - buildAgent(tool: Tool | null) { - if (tool !== null) { + buildAgent(tools: Tool[]) { + if (tools.length) { const chatModel = Private.getChatModel(); if (chatModel === null) { Private.setAgent(null); return; } - chatModel.bindTools?.([tool], { tool_choice: tool.name }); + chatModel.bindTools?.(tools); Private.setChatModel(chatModel); Private.setAgent( createReactAgent({ llm: chatModel, - tools: [tool] + tools }) ); } else { diff --git a/src/tokens.ts b/src/tokens.ts index d0d96c4..f41ac27 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -144,7 +144,7 @@ export interface IAIProviderRegistry { /** * Build an agent with a given tool. */ - buildAgent(tool: Tool | null): void; + buildAgent(tools: Tool[]): void; /** * A signal emitting when the provider or its settings has changed. */ From ce2703b3daf698bcd2d4cdac4da922623f18542b Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Tue, 1 Jul 2025 16:52:40 +0200 Subject: [PATCH 08/10] Update the default tool to create a notebook --- src/index.ts | 4 ++-- src/tools/create-notebook.ts | 37 ++++++++++++++++++++++++++++++++++++ src/tools/test-tool.ts | 17 ----------------- 3 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 src/tools/create-notebook.ts delete mode 100644 src/tools/test-tool.ts diff --git a/src/index.ts b/src/index.ts index a532cf3..5149bd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,7 +29,7 @@ import { AIProviderRegistry } from './provider'; import { aiSettingsRenderer, textArea } from './settings'; import { IAIProviderRegistry, IToolRegistry, PLUGIN_IDS } from './tokens'; import { ToolsRegistry } from './tool-registry'; -import { testTool } from './tools/test-tool'; +import { createNotebook } from './tools/create-notebook'; const chatCommandRegistryPlugin: JupyterFrontEndPlugin = { id: PLUGIN_IDS.chatCommandRegistry, @@ -313,7 +313,7 @@ const toolRegistryPlugin: JupyterFrontEndPlugin = { provides: IToolRegistry, activate: (app: JupyterFrontEnd): IToolRegistry => { const registry = new ToolsRegistry(); - registry.add(testTool); + registry.add(createNotebook(app.commands)); return registry; } }; diff --git a/src/tools/create-notebook.ts b/src/tools/create-notebook.ts new file mode 100644 index 0000000..7a8fec9 --- /dev/null +++ b/src/tools/create-notebook.ts @@ -0,0 +1,37 @@ +import { StructuredToolInterface, tool } from '@langchain/core/tools'; +import { CommandRegistry } from '@lumino/commands'; +import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; +import { z } from 'zod'; + +export const createNotebook = ( + commands: CommandRegistry +): StructuredToolInterface => { + return tool( + async ({ command, args }) => { + let result: any = 'No command called'; + if (command === 'notebook:create-new') { + result = await commands.execute( + command, + args as ReadonlyPartialJSONObject + ); + } + const output = ` +The test tool has been called, with the following query: "${command}" +The args for the commands where ${JSON.stringify(args)} +The result of the command (if called) is "${result}" +`; + return output; + }, + { + name: 'createNotebook', + description: 'Run jupyterlab command to create a notebook', + schema: z.object({ + command: z.string().describe('The Jupyterlab command id to execute'), + args: z + .object({}) + .passthrough() + .describe('The argument for the command') + }) + } + ); +}; diff --git a/src/tools/test-tool.ts b/src/tools/test-tool.ts deleted file mode 100644 index 04007a9..0000000 --- a/src/tools/test-tool.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { showDialog } from '@jupyterlab/apputils'; -import { StructuredToolInterface, tool } from '@langchain/core/tools'; -import { z } from 'zod'; - -export const testTool: StructuredToolInterface = tool( - async ({ query }) => { - showDialog({ title: 'Answer', body: query }); - return 'The test tool has been called'; - }, - { - name: 'testTool', - description: 'Display a modal with the provider answer', - schema: z.object({ - query: z.string().describe('The query to display') - }) - } -); From 2db481621a131f8de193bf2a72b07941a68b41d6 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 4 Jul 2025 22:34:00 +0200 Subject: [PATCH 09/10] Disable the tools button if the provider cannot handle it --- schema/chat.json | 6 --- schema/provider-registry.json | 6 +++ src/chat-handler.ts | 77 +++++++--------------------------- src/components/tool-select.tsx | 40 +++++++++++++----- src/index.ts | 18 ++++---- src/provider.ts | 68 ++++++++++++++++++++++++++---- src/tokens.ts | 16 ++++++- 7 files changed, 138 insertions(+), 93 deletions(-) diff --git a/schema/chat.json b/schema/chat.json index ac2c406..0e79f64 100644 --- a/schema/chat.json +++ b/schema/chat.json @@ -22,12 +22,6 @@ "title": "AI persona name", "description": "The name of the AI persona", "default": "Jupyternaut" - }, - "UseTool": { - "type": "boolean", - "title": "Use tool", - "description": "Whether tools are available in chat or not", - "default": false } }, "additionalProperties": false diff --git a/schema/provider-registry.json b/schema/provider-registry.json index ad5cfe9..a58e244 100644 --- a/schema/provider-registry.json +++ b/schema/provider-registry.json @@ -11,6 +11,12 @@ "description": "Whether to use or not the secrets manager. If not, secrets will be stored in the browser (local storage)", "default": true }, + "AllowToolsUsage": { + "type": "boolean", + "title": "Allow tools usage", + "description": "Whether tools are available in chat or not", + "default": true + }, "UniqueProvider": { "type": "boolean", "title": "Use the same provider for chat and completer", diff --git a/src/chat-handler.ts b/src/chat-handler.ts index 767c89c..a3d0faf 100644 --- a/src/chat-handler.ts +++ b/src/chat-handler.ts @@ -24,11 +24,10 @@ import { SystemMessage } from '@langchain/core/messages'; import { UUID } from '@lumino/coreutils'; -import { ISignal, Signal } from '@lumino/signaling'; import { DEFAULT_CHAT_SYSTEM_PROMPT } from './default-prompts'; import { jupyternautLiteIcon } from './icons'; -import { IAIProviderRegistry, IToolRegistry, Tool } from './tokens'; +import { IAIProviderRegistry, IToolRegistry } from './tokens'; import { AIChatModel } from './types/ai-model'; /** @@ -66,6 +65,13 @@ export class ChatHandler extends AbstractChatModel { }); } + /** + * The provider registry. + */ + get providerRegistry(): IAIProviderRegistry { + return this._providerRegistry; + } + /** * Get the tool registry. */ @@ -104,44 +110,13 @@ export class ChatHandler extends AbstractChatModel { this._personaName = value; } - /** - * Getter/setter for the use of tools. - */ - get useTool(): boolean { - return this._useTool; - } - set useTool(value: boolean) { - if (this._useTool !== value) { - this._useTool = value; - this._useToolChanged.emit(this._useTool); - } - } - - /** - * Get/set a tool, which will build an agent. - */ - get tools(): Tool[] { - return this._tools; - } - set tools(value: Tool[]) { - this._tools = value; - this._providerRegistry.buildAgent(this._tools); - } - - /** - * A signal triggered when the setting on tool usage has changed. - */ - get useToolChanged(): ISignal { - return this._useToolChanged; - } - /** * Get the system prompt for the chat. */ get systemPrompt(): string { let prompt = this._providerRegistry.chatSystemPrompt ?? DEFAULT_CHAT_SYSTEM_PROMPT; - if (this.useTool && this.agent !== null) { + if (this.agent !== null) { prompt = prompt.concat('\nPlease use the tool that is provided'); } return prompt; @@ -194,7 +169,7 @@ export class ChatHandler extends AbstractChatModel { const sender = { username: this._personaName, avatar_url: AI_AVATAR }; this.updateWriters([{ user: sender }]); - if (this._useTool && this.agent !== null) { + if (this.agent !== null) { return this._sendAgentMessage(this.agent, messages, sender); } @@ -337,10 +312,7 @@ export class ChatHandler extends AbstractChatModel { private _history: IChatHistory = { messages: [] }; private _defaultErrorMessage = 'AI provider not configured'; private _controller: AbortController | null = null; - private _useTool: boolean = false; - private _tools: Tool[] = []; private _toolRegistry?: IToolRegistry; - private _useToolChanged = new Signal(this); } export namespace ChatHandler { @@ -359,34 +331,17 @@ export namespace ChatHandler { users = []; /** - * The tool registry. - */ - get toolsRegistry(): IToolRegistry | undefined { - return (this._model as ChatHandler).toolRegistry; - } - - /** - * Whether to use or not the tool. + * The provider registry. */ - get useTool(): boolean { - return (this._model as ChatHandler).useTool; + get providerRegistry(): IAIProviderRegistry { + return (this._model as ChatHandler).providerRegistry; } /** - * A signal triggered when the setting on tool usage has changed. - */ - get useToolChanged(): ISignal { - return (this._model as ChatHandler).useToolChanged; - } - - /** - * Getter/setter of the tool to use. + * The tool registry. */ - get tools(): Tool[] { - return (this._model as ChatHandler).tools; - } - set tools(value: Tool[]) { - (this._model as ChatHandler).tools = value; + get toolsRegistry(): IToolRegistry | undefined { + return (this._model as ChatHandler).toolRegistry; } } diff --git a/src/components/tool-select.tsx b/src/components/tool-select.tsx index eb4d036..ab4d54b 100644 --- a/src/components/tool-select.tsx +++ b/src/components/tool-select.tsx @@ -10,7 +10,7 @@ import { Menu, MenuItem, Tooltip, Typography } from '@mui/material'; import React, { useCallback, useEffect, useState } from 'react'; import { ChatHandler } from '../chat-handler'; -import { Tool } from '../tokens'; +import { IAIProviderRegistry, Tool } from '../tokens'; const SELECT_ITEM_CLASS = 'jp-AIToolSelect-item'; @@ -22,8 +22,10 @@ export function toolSelect( ): JSX.Element { const chatContext = props.model.chatContext as ChatHandler.ChatContext; const toolRegistry = chatContext.toolsRegistry; + const providerRegistry = chatContext.providerRegistry; - const [useTool, setUseTool] = useState(chatContext.useTool); + const [allowTools, setAllowTools] = useState(true); + const [agentAvailable, setAgentAvailable] = useState(); const [selectedTools, setSelectedTools] = useState([]); const [tools, setTools] = useState(toolRegistry?.tools || []); const [menuAnchorEl, setMenuAnchorEl] = useState(null); @@ -47,7 +49,9 @@ export function toolSelect( currentTools.push(tool); } setSelectedTools(currentTools); - chatContext.tools = currentTools; + if (!providerRegistry.setTools(currentTools)) { + setSelectedTools([]); + } }; useEffect(() => { @@ -59,21 +63,37 @@ export function toolSelect( }, [toolRegistry]); useEffect(() => { - const updateUseTool = (_: ChatHandler, value: boolean) => setUseTool(value); - chatContext.useToolChanged.connect(updateUseTool); + const updateAllowTools = (_: IAIProviderRegistry, value: boolean) => + setAllowTools(value); + + const updateAgentAvailable = () => + setAgentAvailable(providerRegistry.isAgentAvailable()); + + providerRegistry.allowToolsChanged.connect(updateAllowTools); + providerRegistry.providerChanged.connect(updateAgentAvailable); + + setAllowTools(providerRegistry.allowTools); + setAgentAvailable(providerRegistry.isAgentAvailable()); return () => { - chatContext.useToolChanged.disconnect(updateUseTool); + providerRegistry.allowToolsChanged.disconnect(updateAllowTools); + providerRegistry.providerChanged.disconnect(updateAgentAvailable); }; - }, [chatContext]); + }, [providerRegistry]); - return useTool && tools.length ? ( + return allowTools && tools.length ? ( <> { openMenu(e.currentTarget); }} - disabled={!tools.length} - tooltip="Tool" + disabled={!agentAvailable} + tooltip={ + agentAvailable === undefined + ? 'The provider is not set' + : agentAvailable + ? 'Tools' + : 'The provider or model cannot use tools' + } buttonProps={{ variant: 'contained', onKeyDown: e => { diff --git a/src/index.ts b/src/index.ts index 5149bd1..142fc51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,19 +83,16 @@ const chatPlugin: JupyterFrontEndPlugin = { let sendWithShiftEnter = false; let enableCodeToolbar = true; let personaName = 'AI'; - let useTool = false; function loadSetting(setting: ISettingRegistry.ISettings): void { sendWithShiftEnter = setting.get('sendWithShiftEnter') .composite as boolean; enableCodeToolbar = setting.get('enableCodeToolbar').composite as boolean; personaName = setting.get('personaName').composite as string; - useTool = (setting.get('UseTool').composite as boolean) ?? false; // set the properties chatHandler.config = { sendWithShiftEnter, enableCodeToolbar }; chatHandler.personaName = personaName; - chatHandler.useTool = useTool; } Promise.all([app.restored, settingsRegistry?.load(chatPlugin.id)]) @@ -208,6 +205,8 @@ const providerRegistryPlugin: JupyterFrontEndPlugin = }) ); + let allowToolsUsage = true; + settingRegistry .load(providerRegistryPlugin.id) .then(settings => { @@ -215,9 +214,14 @@ const providerRegistryPlugin: JupyterFrontEndPlugin = delete settings.schema.properties?.['UseSecretsManager']; } - const updateProvider = () => { + const loadSetting = (setting: ISettingRegistry.ISettings) => { + // Allowing usage of tools in the chat. + allowToolsUsage = + (setting.get('AllowToolsUsage').composite as boolean) ?? false; + providerRegistry.allowTools = allowToolsUsage; + // Get the Ai provider settings. - const providerSettings = settings.get('AIproviders') + const providerSettings = setting.get('AIproviders') .composite as ReadonlyPartialJSONObject; // Update completer provider. @@ -239,8 +243,8 @@ const providerRegistryPlugin: JupyterFrontEndPlugin = } }; - settings.changed.connect(() => updateProvider()); - updateProvider(); + settings.changed.connect(loadSetting); + loadSetting(settings); }) .catch(reason => { console.error( diff --git a/src/provider.ts b/src/provider.ts index c37350d..84fa4f5 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -34,6 +34,7 @@ export class AIProviderRegistry implements IAIProviderRegistry { */ constructor(options: AIProviderRegistry.IOptions) { this._secretsManager = options.secretsManager || null; + this._allowTools = true; Private.setToken(options.token); this._notifications = { @@ -159,6 +160,16 @@ export class AIProviderRegistry implements IAIProviderRegistry { this._chatPrompt = value; } + /** + * Check if we can add tools to the chat model to build an agent. + */ + isAgentAvailable(): boolean | undefined { + if (Private.getChatModel() === null) { + return; + } + return Private.getChatModel()?.bindTools !== undefined; + } + /** * Get the settings schema of a given provider. */ @@ -343,6 +354,11 @@ export class AIProviderRegistry implements IAIProviderRegistry { ...fullSettings }) ); + if (this.isAgentAvailable() && this._allowTools) { + if (this._tools.length) { + this._buildAgent(); + } + } } catch (e: any) { this.chatError = e.message; Private.setChatModel(null); @@ -354,6 +370,38 @@ export class AIProviderRegistry implements IAIProviderRegistry { this._providerChanged.emit('chat'); } + /** + * Allowing the usage of tools from settings. + */ + get allowTools(): boolean { + return this._allowTools; + } + set allowTools(value: boolean) { + if (this._allowTools !== value) { + this._allowTools = value; + this._allowToolsChanged.emit(value); + } + } + + /** + * Set the tools to use with the chat. + */ + setTools(tools: Tool[]): boolean { + if (!this.isAgentAvailable()) { + this._tools = []; + return false; + } + this._tools = tools; + return this._buildAgent(); + } + + /** + * A signal triggered when the setting on tool usage has changed. + */ + get allowToolsChanged(): ISignal { + return this._allowToolsChanged; + } + /** * A signal emitting when the provider or its settings has changed. */ @@ -389,26 +437,29 @@ export class AIProviderRegistry implements IAIProviderRegistry { } /** - * Build an agent with a given tool. + * Build an agent with given tools. */ - buildAgent(tools: Tool[]) { - if (tools.length) { + private _buildAgent(): boolean { + console.log('Build Agent'); + if (this._tools.length) { const chatModel = Private.getChatModel(); - if (chatModel === null) { + if (chatModel === null || chatModel.bindTools === undefined) { Private.setAgent(null); - return; + this._tools = []; + return false; } - chatModel.bindTools?.(tools); + chatModel.bindTools?.(this._tools); Private.setChatModel(chatModel); Private.setAgent( createReactAgent({ llm: chatModel, - tools + tools: this._tools }) ); } else { Private.setAgent(null); } + return true; } private _secretsManager: ISecretsManager | null; @@ -426,6 +477,9 @@ export class AIProviderRegistry implements IAIProviderRegistry { }; private _chatPrompt: string = ''; private _completerPrompt: string = ''; + private _allowTools: boolean; + private _allowToolsChanged = new Signal(this); + private _tools: Tool[] = []; } export namespace AIProviderRegistry { diff --git a/src/tokens.ts b/src/tokens.ts index f41ac27..9d02124 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -109,6 +109,10 @@ export interface IAIProviderRegistry { * Getter/setter for the chat system prompt. */ chatSystemPrompt: string; + /** + * Check if tools can be added to the chat model, to build an agent. + */ + isAgentAvailable(): boolean | undefined; /** * Get the settings schema of a given provider. */ @@ -142,9 +146,17 @@ export interface IAIProviderRegistry { */ setChatProvider(settings: ReadonlyPartialJSONObject): void; /** - * Build an agent with a given tool. + * Allowing the usage of tools from settings. + */ + allowTools: boolean; + /** + * Set the tools to use with the chat. + */ + setTools(tools: Tool[]): boolean; + /** + * A signal triggered when the ability to use tools changed. */ - buildAgent(tools: Tool[]): void; + readonly allowToolsChanged: ISignal; /** * A signal emitting when the provider or its settings has changed. */ From 037a4d0483e7b5b303738874fae41b4d4101e719 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Tue, 8 Jul 2025 10:03:54 +0200 Subject: [PATCH 10/10] Ignore langgraph warning for now (#2) --- .eslintignore | 1 + package.json | 3 ++- webpack.config.js | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 webpack.config.js diff --git a/.eslintignore b/.eslintignore index 6f8fb52..834d381 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,3 +5,4 @@ coverage tests **/__tests__ ui-tests +webpack.config.js diff --git a/package.json b/package.json index 2bf60c0..77ae803 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "jupyterlab": { "extension": true, "outputDir": "jupyterlite_ai/labextension", - "schemaDir": "schema" + "schemaDir": "schema", + "webpackConfig": "./webpack.config.js" } } diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..e8031e3 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,9 @@ +module.exports = { + // Ignore source map warnings for @langchain/langgraph + ignoreWarnings: [ + { + module: /node_modules\/@langchain\/langgraph/ + }, + /Failed to parse source map.*@langchain/ + ] +};