Skip to content

Commit 4691e5f

Browse files
committed
Include langgraph to call tool from the chat prompt
1 parent 258d16d commit 4691e5f

File tree

9 files changed

+290
-49
lines changed

9 files changed

+290
-49
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"@jupyterlab/ui-components": "^4.4.0",
6868
"@langchain/anthropic": "^0.3.9",
6969
"@langchain/community": "^0.3.44",
70-
"@langchain/core": "^0.3.57",
70+
"@langchain/core": "^0.3.61",
71+
"@langchain/langgraph": "^0.3.5",
7172
"@langchain/mistralai": "^0.1.1",
7273
"@langchain/ollama": "^0.2.0",
7374
"@langchain/openai": "^0.4.4",

schema/provider-registry.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
"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.",
1818
"default": true
1919
},
20+
"UseAgent": {
21+
"type": "boolean",
22+
"title": "Use agent in chat",
23+
"description": "Whether to use or not an agent in chat",
24+
"default": false
25+
},
2026
"AIproviders": {
2127
"type": "object",
2228
"title": "AI providers",

schema/system-prompts.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"type": "string",
1010
"title": "Chat message system prompt",
1111
"description": "The system prompt for the chat messages",
12-
"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."
12+
"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."
1313
},
1414
"completionSystemPrompt": {
1515
"type": "string",

src/chat-handler.ts

Lines changed: 114 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ import {
1313
IChatMessage,
1414
IChatModel,
1515
IInputModel,
16-
INewMessage
16+
INewMessage,
17+
IUser
1718
} from '@jupyter/chat';
1819
import {
1920
AIMessage,
21+
BaseMessage,
2022
HumanMessage,
2123
mergeMessageRuns,
2224
SystemMessage
2325
} from '@langchain/core/messages';
26+
import { CompiledStateGraph } from '@langchain/langgraph';
2427
import { UUID } from '@lumino/coreutils';
2528

2629
import { DEFAULT_CHAT_SYSTEM_PROMPT } from './default-prompts';
@@ -62,7 +65,11 @@ export class ChatHandler extends AbstractChatModel {
6265
});
6366
}
6467

65-
get provider(): AIChatModel | null {
68+
get agent(): CompiledStateGraph<any, any> | null {
69+
return this._providerRegistry.currentAgent;
70+
}
71+
72+
get chatModel(): AIChatModel | null {
6673
return this._providerRegistry.currentChatModel;
6774
}
6875

@@ -110,7 +117,9 @@ export class ChatHandler extends AbstractChatModel {
110117
};
111118
this.messageAdded(msg);
112119

113-
if (this._providerRegistry.currentChatModel === null) {
120+
const chatModel = this.chatModel;
121+
122+
if (chatModel === null) {
114123
const errorMsg: IChatMessage = {
115124
id: UUID.uuid4(),
116125
body: `**${this._errorMessage ? this._errorMessage : this._defaultErrorMessage}**`,
@@ -137,23 +146,52 @@ export class ChatHandler extends AbstractChatModel {
137146
const sender = { username: this._personaName, avatar_url: AI_AVATAR };
138147
this.updateWriters([{ user: sender }]);
139148

140-
// create an empty message to be filled by the AI provider
149+
if (this._providerRegistry.useAgent && this.agent !== null) {
150+
return this._sendAgentMessage(this.agent, messages, sender);
151+
}
152+
153+
return this._sentChatMessage(chatModel, messages, sender);
154+
}
155+
156+
async getHistory(): Promise<IChatHistory> {
157+
return this._history;
158+
}
159+
160+
dispose(): void {
161+
super.dispose();
162+
}
163+
164+
messageAdded(message: IChatMessage): void {
165+
super.messageAdded(message);
166+
}
167+
168+
stopStreaming(): void {
169+
this._controller?.abort();
170+
}
171+
172+
createChatContext(): IChatContext {
173+
return new ChatHandler.ChatContext({ model: this });
174+
}
175+
176+
private async _sentChatMessage(
177+
chatModel: AIChatModel,
178+
messages: BaseMessage[],
179+
sender: IUser
180+
): Promise<boolean> {
181+
// Create an empty message to be filled by the AI provider
141182
const botMsg: IChatMessage = {
142183
id: UUID.uuid4(),
143184
body: '',
144185
sender,
145186
time: Private.getTimestampMs(),
146187
type: 'msg'
147188
};
148-
149189
let content = '';
150-
151190
this._controller = new AbortController();
152191
try {
153-
for await (const chunk of await this._providerRegistry.currentChatModel.stream(
154-
messages,
155-
{ signal: this._controller.signal }
156-
)) {
192+
for await (const chunk of await chatModel.stream(messages, {
193+
signal: this._controller.signal
194+
})) {
157195
content += chunk.content ?? chunk;
158196
botMsg.body = content;
159197
this.messageAdded(botMsg);
@@ -177,26 +215,73 @@ export class ChatHandler extends AbstractChatModel {
177215
}
178216
}
179217

180-
async getHistory(): Promise<IChatHistory> {
181-
return this._history;
182-
}
183-
184-
dispose(): void {
185-
super.dispose();
186-
}
187-
188-
messageAdded(message: IChatMessage): void {
189-
super.messageAdded(message);
190-
}
191-
192-
stopStreaming(): void {
193-
this._controller?.abort();
194-
}
195-
196-
createChatContext(): IChatContext {
197-
return new ChatHandler.ChatContext({ model: this });
218+
private async _sendAgentMessage(
219+
agent: CompiledStateGraph<any, any>,
220+
messages: BaseMessage[],
221+
sender: IUser
222+
): Promise<boolean> {
223+
this._controller = new AbortController();
224+
try {
225+
for await (const chunk of await agent.stream(
226+
{ messages },
227+
{
228+
streamMode: 'updates',
229+
signal: this._controller.signal
230+
}
231+
)) {
232+
if ((chunk as any).agent) {
233+
messages = (chunk as any).agent.messages;
234+
messages.forEach(message => {
235+
const contents: string[] = [];
236+
if (typeof message.content === 'string') {
237+
contents.push(message.content);
238+
} else if (Array.isArray(message.content)) {
239+
message.content.forEach(content => {
240+
if (content.type === 'text') {
241+
contents.push(content.text);
242+
}
243+
});
244+
}
245+
contents.forEach(content => {
246+
this.messageAdded({
247+
id: UUID.uuid4(),
248+
body: content,
249+
sender,
250+
time: Private.getTimestampMs(),
251+
type: 'msg'
252+
});
253+
});
254+
});
255+
} else if ((chunk as any).tools) {
256+
messages = (chunk as any).tools.messages;
257+
messages.forEach(message => {
258+
this.messageAdded({
259+
id: UUID.uuid4(),
260+
body: message.content as string,
261+
sender: { username: `Tool "${message.name}"` },
262+
time: Private.getTimestampMs(),
263+
type: 'msg'
264+
});
265+
});
266+
}
267+
}
268+
return true;
269+
} catch (reason) {
270+
const error = this._providerRegistry.formatErrorMessage(reason);
271+
const errorMsg: IChatMessage = {
272+
id: UUID.uuid4(),
273+
body: `**${error}**`,
274+
sender: { username: 'ERROR' },
275+
time: Private.getTimestampMs(),
276+
type: 'msg'
277+
};
278+
this.messageAdded(errorMsg);
279+
return false;
280+
} finally {
281+
this.updateWriters([]);
282+
this._controller = null;
283+
}
198284
}
199-
200285
private _providerRegistry: IAIProviderRegistry;
201286
private _personaName = 'AI';
202287
private _errorMessage: string = '';

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,13 @@ const providerRegistryPlugin: JupyterFrontEndPlugin<IAIProviderRegistry> =
203203
if (!secretsManager) {
204204
delete settings.schema.properties?.['UseSecretsManager'];
205205
}
206+
206207
const updateProvider = () => {
208+
// Update agent usage if necessary.
209+
const useAgent =
210+
(settings.get('UseAgent').composite as boolean) ?? false;
211+
providerRegistry.useAgent = useAgent;
212+
207213
// Get the Ai provider settings.
208214
const providerSettings = settings.get('AIproviders')
209215
.composite as ReadonlyPartialJSONObject;

src/provider.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
IInlineCompletionContext
44
} from '@jupyterlab/completer';
55
import { BaseLanguageModel } from '@langchain/core/language_models/base';
6+
import { CompiledStateGraph } from '@langchain/langgraph';
7+
import { createReactAgent } from '@langchain/langgraph/prebuilt';
68
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
79
import { ISignal, Signal } from '@lumino/signaling';
810
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
@@ -18,6 +20,7 @@ import {
1820
ModelRole,
1921
PLUGIN_IDS
2022
} from './tokens';
23+
import { testTool } from './tools/test_tool';
2124
import { AIChatModel, AICompleter } from './types/ai-model';
2225

2326
const SECRETS_NAMESPACE = PLUGIN_IDS.providerRegistry;
@@ -153,6 +156,17 @@ export class AIProviderRegistry implements IAIProviderRegistry {
153156
};
154157
}
155158

159+
/**
160+
* Get the current agent.
161+
*/
162+
get currentAgent(): CompiledStateGraph<any, any> | null {
163+
const agent = Private.getAgent();
164+
if (agent === null) {
165+
return null;
166+
}
167+
return agent;
168+
}
169+
156170
/**
157171
* Getter/setter for the chat system prompt.
158172
*/
@@ -166,6 +180,20 @@ export class AIProviderRegistry implements IAIProviderRegistry {
166180
this._chatPrompt = value;
167181
}
168182

183+
/**
184+
* Getter/setter for the use of agent in chat.
185+
*/
186+
get useAgent(): boolean {
187+
return this._useAgentInChat;
188+
}
189+
set useAgent(value: boolean) {
190+
if (value === this._useAgentInChat) {
191+
return;
192+
}
193+
this._useAgentInChat = value;
194+
this._buildAgent();
195+
}
196+
169197
/**
170198
* Get the settings schema of a given provider.
171199
*/
@@ -329,6 +357,9 @@ export class AIProviderRegistry implements IAIProviderRegistry {
329357
...fullSettings
330358
})
331359
);
360+
if (this._useAgentInChat) {
361+
this._buildAgent();
362+
}
332363
} catch (e: any) {
333364
this._chatError = e.message;
334365
Private.setChatModel(null);
@@ -374,6 +405,29 @@ export class AIProviderRegistry implements IAIProviderRegistry {
374405
return fullSettings;
375406
}
376407

408+
/**
409+
* Build an agent.
410+
*/
411+
private _buildAgent() {
412+
if (this._useAgentInChat) {
413+
const chatModel = Private.getChatModel();
414+
if (chatModel === null) {
415+
Private.setAgent(null);
416+
return;
417+
}
418+
chatModel.bindTools?.([testTool]);
419+
Private.setChatModel(chatModel);
420+
Private.setAgent(
421+
createReactAgent({
422+
llm: chatModel,
423+
tools: [testTool]
424+
})
425+
);
426+
} else {
427+
Private.setAgent(null);
428+
}
429+
}
430+
377431
private _secretsManager: ISecretsManager | null;
378432
private _providerChanged = new Signal<IAIProviderRegistry, ModelRole>(this);
379433
private _chatError: string = '';
@@ -386,6 +440,7 @@ export class AIProviderRegistry implements IAIProviderRegistry {
386440
};
387441
private _chatPrompt: string = '';
388442
private _completerPrompt: string = '';
443+
private _useAgentInChat = false;
389444
}
390445

391446
export namespace AIProviderRegistry {
@@ -510,4 +565,15 @@ namespace Private {
510565
export function getCompleter(): IBaseCompleter | null {
511566
return completer;
512567
}
568+
569+
/**
570+
* The agent getter and setter.
571+
*/
572+
let agent: CompiledStateGraph<any, any> | null = null;
573+
export function setAgent(value: CompiledStateGraph<any, any> | null): void {
574+
agent = value;
575+
}
576+
export function getAgent(): CompiledStateGraph<any, any> | null {
577+
return agent;
578+
}
513579
}

src/tokens.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CompiledStateGraph } from '@langchain/langgraph';
12
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
23
import { ReadonlyPartialJSONObject, Token } from '@lumino/coreutils';
34
import { ISignal } from '@lumino/signaling';
@@ -90,19 +91,27 @@ export interface IAIProviderRegistry {
9091
/**
9192
* Get the current completer of the completion provider.
9293
*/
93-
currentCompleter: AICompleter | null;
94+
readonly currentCompleter: AICompleter | null;
9495
/**
9596
* Getter/setter for the completer system prompt.
9697
*/
9798
completerSystemPrompt: string;
9899
/**
99100
* Get the current llm chat model.
100101
*/
101-
currentChatModel: AIChatModel | null;
102+
readonly currentChatModel: AIChatModel | null;
103+
/**
104+
* Get the current agent.
105+
*/
106+
readonly currentAgent: CompiledStateGraph<any, any> | null;
102107
/**
103108
* Getter/setter for the chat system prompt.
104109
*/
105110
chatSystemPrompt: string;
111+
/**
112+
* Getter/setter for the use of agent in chat.
113+
*/
114+
useAgent: boolean;
106115
/**
107116
* Get the settings schema of a given provider.
108117
*/

0 commit comments

Comments
 (0)