Skip to content

Commit 2ef7149

Browse files
committed
chore: langchainjs + lmstudio-embedding by OpenAIClient
1 parent 7c0a03c commit 2ef7149

11 files changed

+660
-16
lines changed

.vscode/launch.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Debug test-embedding-local.ts",
6+
"type": "node",
7+
"request": "launch",
8+
"program": "${workspaceFolder}/node_modules/.bin/tsx",
9+
"args": ["langgraph/test-embedding-local.ts"],
10+
"cwd": "${workspaceFolder}",
11+
"env": {},
12+
"console": "integratedTerminal",
13+
"skipFiles": ["<node_internals>/**"],
14+
"sourceMaps": true,
15+
"outFiles": ["${workspaceFolder}/**/*.js"]
16+
}
17+
]
18+
}

langgraph/graph-rag-eg-agentic.ts renamed to langgraph/graph-rag-eg-docs-grading.ts

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// import '@dotenvx/dotenvx/config';
2+
// import "cheerio";
3+
14
import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio';
25
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
36
import { ChatPromptTemplate } from '@langchain/core/prompts';
@@ -10,6 +13,8 @@ import { createRetrieverTool } from 'langchain/tools/retriever';
1013
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
1114
import { z } from 'zod';
1215

16+
// 🧑‍🏫 [LangGraph Retrieval Agent](https://langchain-ai.github.io/langgraphjs/tutorials/rag/langgraph_agentic_rag/)
17+
1318
const urls = [
1419
'https://lilianweng.github.io/posts/2023-06-23-agent/',
1520
'https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/',
@@ -20,39 +25,60 @@ const docs = await Promise.all(
2025
urls.map((url) => new CheerioWebBaseLoader(url).load()),
2126
);
2227
const docsList = docs.flat();
28+
console.log(';; docsList ', docsList[0].pageContent.length);
29+
console.log(';; docsList ', docsList[0]);
2330

2431
const textSplitter = new RecursiveCharacterTextSplitter({
2532
chunkSize: 500,
2633
chunkOverlap: 50,
2734
});
2835
const docSplits = await textSplitter.splitDocuments(docsList);
29-
36+
console.log(';; docSplits ', docSplits.length);
37+
38+
const embeddings = new OpenAIEmbeddings({
39+
// model: "text-embedding-3-large",
40+
model: 'text-embedding-embeddinggemma-300m',
41+
configuration: {
42+
baseURL: 'http://localhost:1234/v1',
43+
apiKey: 'not-needed',
44+
},
45+
});
3046
// save embeddings to vectorDB
3147
const vectorStore = await MemoryVectorStore.fromDocuments(
3248
docSplits,
33-
new OpenAIEmbeddings(),
49+
embeddings,
3450
);
3551

3652
const retriever = vectorStore.asRetriever();
3753

54+
// pass a custom state object to the graph, or use a simple list of messages.
3855
const GraphState = Annotation.Root({
3956
messages: Annotation<BaseMessage[]>({
4057
reducer: (x, y) => x.concat(y),
4158
default: () => [],
4259
}),
4360
});
4461

45-
const tool = createRetrieverTool(retriever, {
62+
const retriveTool = createRetrieverTool(retriever, {
4663
name: 'retrieve_blog_posts',
4764
description:
4865
'Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.',
4966
});
50-
const tools = [tool];
67+
const retriveTools = [retriveTool];
5168

52-
const toolNode = new ToolNode<typeof GraphState.State>(tools);
69+
const retriveToolNode = new ToolNode<typeof GraphState.State>(retriveTools);
5370

71+
// const model = new ChatOpenAI({
72+
// model: 'gpt-4o',
73+
// temperature: 0,
74+
// });
5475
const model = new ChatOpenAI({
55-
model: 'gpt-4o',
76+
// model: 'qwen/qwen3-4b-2507',
77+
model: 'google/gemma-3-12b',
78+
configuration: {
79+
baseURL: 'http://localhost:1234/v1',
80+
apiKey: 'not-needed',
81+
},
5682
temperature: 0,
5783
});
5884

@@ -65,18 +91,19 @@ const model = new ChatOpenAI({
6591
*/
6692
function shouldRetrieve(state: typeof GraphState.State): string {
6793
const { messages } = state;
68-
console.log('---DECIDE TO RETRIEVE---');
6994
const lastMessage = messages[messages.length - 1];
7095

7196
if (
7297
'tool_calls' in lastMessage &&
7398
Array.isArray(lastMessage.tool_calls) &&
7499
lastMessage.tool_calls.length
75100
) {
76-
console.log('---DECISION: RETRIEVE---');
101+
console.log('---RETRIEVE DECISION: YES ✅ ---');
77102
return 'retrieve';
78103
}
104+
79105
// If there are no tool calls then we finish.
106+
console.log('---RETRIEVE DECISION: NO ---');
80107
return END;
81108
}
82109

@@ -92,7 +119,7 @@ function shouldRetrieve(state: typeof GraphState.State): string {
92119
async function gradeDocuments(
93120
state: typeof GraphState.State,
94121
): Promise<Partial<typeof GraphState.State>> {
95-
console.log('---GET RELEVANCE---');
122+
console.log('---NODE gradeDocuments---');
96123

97124
const { messages } = state;
98125
const tool = {
@@ -155,10 +182,10 @@ function checkRelevance(state: typeof GraphState.State): string {
155182
}
156183

157184
if (toolCalls[0].args.binaryScore === 'yes') {
158-
console.log('---DECISION: DOCS RELEVANT---');
185+
console.log('---RELEVANT DECISION: YES---');
159186
return 'yes';
160187
}
161-
console.log('---DECISION: DOCS NOT RELEVANT---');
188+
console.log('---RELEVANT DECISION: NO ---');
162189
return 'no';
163190
}
164191

@@ -174,7 +201,7 @@ function checkRelevance(state: typeof GraphState.State): string {
174201
async function agent(
175202
state: typeof GraphState.State,
176203
): Promise<Partial<typeof GraphState.State>> {
177-
console.log('---CALL AGENT---');
204+
console.log('---Node AGENT---');
178205

179206
const { messages } = state;
180207
// Find the AIMessage which contains the `give_relevance_score` tool call,
@@ -196,7 +223,7 @@ async function agent(
196223
// temperature: 0,
197224
// streaming: true,
198225
// }).bindTools(tools);
199-
const mainChatModel = model.bindTools(tools);
226+
const mainChatModel = model.bindTools(retriveTools);
200227
const response = await mainChatModel.invoke(filteredMessages);
201228
return {
202229
messages: [response],
@@ -252,7 +279,7 @@ async function generate(
252279
const lastToolMessage = messages
253280
.slice()
254281
.reverse()
255-
.find((msg) => msg._getType() === 'tool');
282+
.find((msg) => msg.getType() === 'tool');
256283
if (!lastToolMessage) {
257284
throw new Error('No tool message found in the conversation history');
258285
}
@@ -282,7 +309,7 @@ async function generate(
282309
const workflow = new StateGraph(GraphState)
283310
// Define the nodes which we'll cycle between.
284311
.addNode('agent', agent)
285-
.addNode('retrieve', toolNode)
312+
.addNode('retrieve', retriveToolNode)
286313
.addNode('gradeDocuments', gradeDocuments)
287314
.addNode('rewrite', rewrite)
288315
.addNode('generate', generate);
@@ -307,7 +334,7 @@ workflow.addConditionalEdges(
307334
{
308335
// Call tool node
309336
yes: 'generate',
310-
no: 'rewrite', // placeholder
337+
no: 'rewrite',
311338
},
312339
);
313340

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio';
2+
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
3+
import { ChatPromptTemplate, PromptTemplate } from '@langchain/core/prompts';
4+
import { Annotation, END, START, StateGraph } from '@langchain/langgraph';
5+
import { ToolNode } from '@langchain/langgraph/prebuilt';
6+
import { ChatOpenAI, OpenAIClient, OpenAIEmbeddings } from '@langchain/openai';
7+
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
8+
import { pull } from 'langchain/hub';
9+
import { createRetrieverTool } from 'langchain/tools/retriever';
10+
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
11+
import { z } from 'zod';
12+
import { Document } from '@langchain/core/documents';
13+
14+
// 🧑‍🏫 [Build a RAG App: Part 1](https://js.langchain.com/docs/tutorials/rag/)
15+
16+
// const model = new ChatOpenAI({
17+
// model: 'gpt-4o',
18+
// temperature: 0,
19+
// });
20+
const model = new ChatOpenAI({
21+
// model: 'qwen/qwen3-4b-2507',
22+
model: 'google/gemma-3-12b',
23+
configuration: {
24+
baseURL: 'http://localhost:1234/v1',
25+
apiKey: 'not-needed',
26+
},
27+
temperature: 0,
28+
});
29+
30+
// const embeddings = new OpenAIEmbeddings({
31+
// model: "text-embedding-qwen3-embedding-0.6b",
32+
// // model: 'text-embedding-embeddinggemma-300m',
33+
// configuration: {
34+
// baseURL: 'http://localhost:1234/v1',
35+
// // check: false,
36+
// apiKey: 'not-needed',
37+
// },
38+
// });
39+
40+
const urls = [
41+
'https://dev.to/nyxtom/introduction-to-crdts-for-realtime-collaboration-2eb1',
42+
'https://dev.to/foxgem/crdts-achieving-eventual-consistency-in-distributed-systems-296g',
43+
// "https://lilianweng.github.io/posts/2023-06-23-agent/",
44+
];
45+
46+
const docs = await Promise.all(
47+
urls.map((url) =>
48+
new CheerioWebBaseLoader(url, {
49+
selector: '.crayons-layout__content',
50+
// selector: 'p'
51+
}).load(),
52+
),
53+
);
54+
const docsList = docs.flat();
55+
56+
// const cheerioLoader = new CheerioWebBaseLoader(
57+
// "https://lilianweng.github.io/posts/2023-06-23-agent/",
58+
// {
59+
// selector: 'p'
60+
// }
61+
// );
62+
// const docsList = await cheerioLoader.load();
63+
64+
console.log(';; docsList ', docsList[0].pageContent.length);
65+
// console.log(';; docsList ', docsList[0].pageContent.slice(0, 2200))
66+
67+
const textSplitter = new RecursiveCharacterTextSplitter({
68+
chunkSize: 500,
69+
chunkOverlap: 50,
70+
});
71+
const docSplits = await textSplitter.splitDocuments(docsList);
72+
console.log(';; docSplits ', docSplits.length);
73+
// console.log(';; docSplits ', docSplits.slice(0, 6))
74+
75+
// 🛢️ save embeddings to vectorDB
76+
// const vectorStore = new MemoryVectorStore(embeddings);
77+
// await vectorStore.addDocuments(docSplits)
78+
// const vectorStore = await MemoryVectorStore.fromDocuments(
79+
// docSplits,
80+
// embeddings
81+
// );
82+
const openAiClient = new OpenAIClient({
83+
apiKey: 'not-needed',
84+
baseURL: 'http://localhost:1234/v1',
85+
});
86+
87+
// Create a proper embeddings interface for OpenAIClient
88+
class OpenAIClientEmbeddings {
89+
constructor(
90+
private client: OpenAIClient,
91+
private model: string,
92+
) {}
93+
94+
async embedDocuments(texts: string[]): Promise<number[][]> {
95+
const response = await this.client.embeddings.create({
96+
model: this.model,
97+
input: texts,
98+
encoding_format: 'float',
99+
});
100+
return response.data.map((item) => item.embedding);
101+
}
102+
103+
async embedQuery(text: string): Promise<number[]> {
104+
const embeddings = await this.embedDocuments([text]);
105+
return embeddings[0];
106+
}
107+
}
108+
109+
// Create embeddings instance and use fromDocuments
110+
const embeddingsInstance = new OpenAIClientEmbeddings(
111+
openAiClient,
112+
'text-embedding-qwen3-embedding-0.6b',
113+
);
114+
// const embeddingsInstance = new OpenAIClientEmbeddings(openAiClient, 'text-embedding-embeddinggemma-300m');
115+
// const embeddingsInstance = new OpenAIClientEmbeddings(openAiClient, 'text-embedding-granite-embedding-278m-multilingual');
116+
const vectorStore = await MemoryVectorStore.fromDocuments(
117+
docSplits,
118+
embeddingsInstance,
119+
);
120+
121+
// const retrievedDocs = await vectorStore.similaritySearch('yjs');
122+
// console.log(';; retrievedDocs ', retrievedDocs.length)
123+
// console.log(';; retrievedDocs ', retrievedDocs)
124+
125+
// Define state for application
126+
const StateAnnotation = Annotation.Root({
127+
question: Annotation<string>,
128+
context: Annotation<Document[]>,
129+
answer: Annotation<string>,
130+
});
131+
132+
// only used for types
133+
const InputStateAnnotation = Annotation.Root({
134+
question: Annotation<string>,
135+
});
136+
137+
// retrieve node
138+
const retrieve = async (state: typeof InputStateAnnotation.State) => {
139+
const retrievedDocs = await vectorStore.similaritySearch(state.question);
140+
console.log(';; retrievedDocs ', retrievedDocs.length);
141+
// console.log(';; retrievedDocs ', retrievedDocs)
142+
return { context: retrievedDocs };
143+
};
144+
145+
const generate = async (state: typeof StateAnnotation.State) => {
146+
const docsContent = state.context.map((doc) => doc.pageContent).join('\n');
147+
148+
// Define prompt for question-answering
149+
// const promptTemplate = await pull<ChatPromptTemplate>("rlm/rag-prompt");
150+
const promptTemplate = PromptTemplate.fromTemplate(
151+
`You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
152+
Question: {question}
153+
Context: {context}
154+
Answer:
155+
`,
156+
);
157+
158+
const messages = await promptTemplate.invoke({
159+
question: state.question,
160+
context: docsContent,
161+
});
162+
163+
const response = await model.invoke(messages);
164+
return { answer: response.content };
165+
};
166+
167+
// Compile application and test
168+
const graph = new StateGraph(StateAnnotation)
169+
.addNode('retrieve', retrieve)
170+
.addNode('generate', generate)
171+
.addEdge('__start__', 'retrieve')
172+
.addEdge('retrieve', 'generate')
173+
.addEdge('generate', '__end__')
174+
.compile();
175+
176+
// -------
177+
178+
let inputs = { question: 'What is CmRDTs ?' };
179+
// let inputs = { question: "What is yjs ?" };
180+
181+
const result = await graph.invoke(inputs);
182+
183+
console.log('\n👾');
184+
console.log(result.answer);

0 commit comments

Comments
 (0)