Skip to content

Commit 848fc71

Browse files
authored
Merge pull request #1664 from liam-hq/add-mastra-to-chat
🔧 Remove `langchain` and add `vercel-ai-sdk` & `mastra`
2 parents d7b9986 + 7ddba41 commit 848fc71

File tree

11 files changed

+2957
-348
lines changed

11 files changed

+2957
-348
lines changed

config/dependency_decisions.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,9 @@
139139
:why: Compatible with Apache-2.0 license. See https://opensource.org/license/artistic-2-0
140140
:versions: []
141141
:when: 2025-05-08 05:24:20.998252000 Z
142+
- - :approve
143+
- "@mastra/core"
144+
- :who: Liam @FunamaYukina
145+
:why: The license is Elastic-2.0. This is not an OSS license, so to be on the safe side, we'll approve it on a package-by-package basis.
146+
:versions: []
147+
:when: 2025-05-13 00:30:00.632489000 Z

frontend/apps/app/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,6 @@ prism.wasm
5252
!.env.local
5353

5454
*storybook.log
55+
56+
# Mastra
57+
.mastra
Lines changed: 38 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import { langfuseHandler } from '@/libs/langfuse/langfuseHandler'
2-
import {} from '@langchain/core/messages'
3-
import { ChatPromptTemplate } from '@langchain/core/prompts'
4-
import { ChatOpenAI } from '@langchain/openai'
1+
import { mastra } from '@/lib/mastra'
52
import type { Schema, TableGroup } from '@liam-hq/db-structure'
6-
import { Document } from 'langchain/document'
3+
import * as Sentry from '@sentry/nextjs'
74
import { NextResponse } from 'next/server'
85

96
// Export TableGroupData type for compatibility
@@ -13,7 +10,7 @@ export type TableGroupData = TableGroup
1310
const tableToDocument = (
1411
tableName: string,
1512
tableData: Schema['tables'][string],
16-
): Document => {
13+
): string => {
1714
// Table description
1815
const tableDescription = `Table: ${tableName}\nDescription: ${tableData.comment || 'No description'}\n`
1916

@@ -39,30 +36,20 @@ const tableToDocument = (
3936
}
4037

4138
// Combine all information
42-
const tableText = `${tableDescription}${columnsText}${primaryKeyText}`
43-
44-
return new Document({
45-
pageContent: tableText,
46-
metadata: { tableName },
47-
})
39+
return `${tableDescription}${columnsText}${primaryKeyText}`
4840
}
4941

5042
// Convert relationship data to text document
5143
const relationshipToDocument = (
5244
relationshipName: string,
5345
relationshipData: Schema['relationships'][string],
54-
): Document => {
55-
const relationshipText = `Relationship: ${relationshipName}
46+
): string => {
47+
return `Relationship: ${relationshipName}
5648
From Table: ${relationshipData.primaryTableName}
5749
From Column: ${relationshipData.primaryColumnName}
5850
To Table: ${relationshipData.foreignTableName}
5951
To Column: ${relationshipData.foreignColumnName}
6052
Type: ${relationshipData.cardinality || 'unknown'}\n`
61-
62-
return new Document({
63-
pageContent: relationshipText,
64-
metadata: { relationshipName },
65-
})
6653
}
6754

6855
// Convert table groups to text document
@@ -99,7 +86,7 @@ const convertSchemaToText = (schema: Schema): string => {
9986
schemaText += 'TABLES:\n\n'
10087
for (const [tableName, tableData] of Object.entries(schema.tables)) {
10188
const tableDoc = tableToDocument(tableName, tableData)
102-
schemaText = `${schemaText}${tableDoc.pageContent}\n\n`
89+
schemaText = `${schemaText}${tableDoc}\n\n`
10390
}
10491
}
10592

@@ -113,7 +100,7 @@ const convertSchemaToText = (schema: Schema): string => {
113100
relationshipName,
114101
relationshipData,
115102
)
116-
schemaText = `${schemaText}${relationshipDoc.pageContent}\n\n`
103+
schemaText = `${schemaText}${relationshipDoc}\n\n`
117104
}
118105
}
119106

@@ -141,7 +128,7 @@ export async function POST(request: Request) {
141128
)
142129
}
143130

144-
// Format chat history for prompt template
131+
// Format chat history for prompt
145132
const formattedChatHistory =
146133
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
147134
history && history.length > 0
@@ -155,126 +142,41 @@ export async function POST(request: Request) {
155142
// Convert schema to text
156143
const schemaText = convertSchemaToText(schemaData)
157144

158-
// Create a streaming model
159-
const streamingModel = new ChatOpenAI({
160-
modelName: 'o4-mini-2025-04-16',
161-
streaming: true,
162-
callbacks: [langfuseHandler],
163-
})
164-
165-
// Create a prompt template with full schema context and chat history
166-
const prompt = ChatPromptTemplate.fromTemplate(`
167-
You are a database schema expert.
168-
Answer questions about the user's schema and provide advice on database design.
169-
Follow these guidelines:
170-
171-
1. Clearly explain the structure of the schema, tables, and relationships.
172-
2. Provide advice based on good database design principles.
173-
3. Share best practices for normalization, indexing, and performance.
174-
4. When using technical terms, include brief explanations.
175-
5. Provide only information directly related to the question, avoiding unnecessary details.
176-
6. Format your responses using GitHub Flavored Markdown (GFM) for better readability.
177-
178-
Your goal is to help users understand and optimize their database schemas.
145+
try {
146+
// Get the agent from Mastra
147+
const agent = mastra.getAgent('databaseSchemaAgent')
148+
if (!agent) {
149+
throw new Error('databaseSchemaAgent not found in Mastra instance')
150+
}
179151

152+
// Create a response using the agent
153+
const response = await agent.generate([
154+
{
155+
role: 'system',
156+
content: `
180157
Complete Schema Information:
181158
${schemaText}
182159
183160
Previous conversation:
184-
{chat_history}
185-
186-
Question: {input}
187-
188-
Based on the schema information provided and considering any previous conversation, answer the question thoroughly and accurately.
189-
`)
190-
191-
// Create streaming chain
192-
const streamingChain = prompt.pipe(streamingModel)
193-
194-
// Generate streaming response
195-
const stream = await streamingChain.stream(
196-
{
197-
input: message,
198-
chat_history: formattedChatHistory,
199-
},
200-
{
201-
callbacks: [langfuseHandler],
202-
metadata: {
203-
endpoint: '/api/chat',
204-
205-
method: 'POST',
206-
messageLength: message.length,
207-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
208-
hasHistory: history ? history.length > 0 : false,
161+
${formattedChatHistory}
162+
`,
209163
},
210-
},
211-
)
212-
213-
// Create a TransformStream to convert the LangChain stream to a ReadableStream
214-
const encoder = new TextEncoder()
215-
const { readable, writable } = new TransformStream()
216-
const writer = writable.getWriter()
217-
218-
// Define types for content processing
219-
type ContentItem = string | { type: string; text: string } | unknown
220-
221-
// Extract content processing to a separate function
222-
const extractTextContent = (
223-
content: string | ContentItem[] | unknown,
224-
): string => {
225-
if (typeof content === 'string') {
226-
return content
227-
}
228-
229-
if (!Array.isArray(content)) {
230-
return ''
231-
}
232-
233-
// Process array content
234-
return content.reduce((text, item) => {
235-
if (typeof item === 'string') {
236-
return text + item
237-
}
238-
239-
if (
240-
item &&
241-
typeof item === 'object' &&
242-
'type' in item &&
243-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
244-
item.type === 'text' &&
245-
'text' in item &&
246-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
247-
typeof item.text === 'string'
248-
) {
249-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
250-
return text + item.text
251-
}
252-
253-
return text
254-
}, '')
255-
}
164+
{
165+
role: 'user',
166+
content: message,
167+
},
168+
])
256169

257-
// Main stream processing function - simplified
258-
const processStream = async () => {
259-
try {
260-
for await (const chunk of stream) {
261-
const textContent = extractTextContent(chunk.content)
262-
await writer.write(encoder.encode(textContent))
263-
}
264-
} catch (error) {
265-
console.error('Error processing stream:', error)
266-
} finally {
267-
await writer.close()
268-
}
170+
return new Response(response.text, {
171+
headers: {
172+
'Content-Type': 'text/plain; charset=utf-8',
173+
},
174+
})
175+
} catch (error) {
176+
Sentry.captureException(error)
177+
return NextResponse.json(
178+
{ error: 'Failed to generate response' },
179+
{ status: 500 },
180+
)
269181
}
270-
271-
// Execute the processing function
272-
processStream()
273-
274-
// Return the streaming response
275-
return new Response(readable, {
276-
headers: {
277-
'Content-Type': 'text/plain; charset=utf-8',
278-
},
279-
})
280182
}

frontend/apps/app/instrumentation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
import { validateConfig } from '@liam-hq/github'
22
import * as Sentry from '@sentry/nextjs'
3+
import { registerOTel } from '@vercel/otel'
4+
import { LangfuseExporter } from 'langfuse-vercel'
35

46
export async function register() {
7+
registerOTel({
8+
serviceName: 'liam-app',
9+
traceExporter: new LangfuseExporter({
10+
publicKey: process.env.LANGFUSE_PUBLIC_KEY || '',
11+
secretKey: process.env.LANGFUSE_SECRET_KEY || '',
12+
baseUrl: process.env.LANGFUSE_BASE_URL || 'https://cloud.langfuse.com',
13+
environment: process.env.NEXT_PUBLIC_ENV_NAME || 'development',
14+
}),
15+
})
16+
517
if (process.env.NEXT_RUNTIME === 'nodejs') {
618
await import('./sentry.server.config')
719
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { openai } from '@ai-sdk/openai'
2+
import type { Metric } from '@mastra/core'
3+
import { Agent, type ToolsInput } from '@mastra/core/agent'
4+
5+
export const databaseSchemaAgent: Agent<
6+
'Database Schema Expert',
7+
ToolsInput,
8+
Record<string, Metric>
9+
> = new Agent({
10+
name: 'Database Schema Expert',
11+
instructions: `
12+
You are a database schema expert.
13+
Answer questions about the user's schema and provide advice on database design.
14+
Follow these guidelines:
15+
16+
1. Clearly explain the structure of the schema, tables, and relationships.
17+
2. Provide advice based on good database design principles.
18+
3. Share best practices for normalization, indexing, and performance.
19+
4. When using technical terms, include brief explanations.
20+
5. Provide only information directly related to the question, avoiding unnecessary details.
21+
6. Format your responses using GitHub Flavored Markdown (GFM) for better readability.
22+
23+
Your goal is to help users understand and optimize their database schemas.
24+
`,
25+
model: openai('o4-mini-2025-04-16'),
26+
})

frontend/apps/app/lib/mastra/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Mastra } from '@mastra/core'
2+
import { LangfuseExporter } from 'langfuse-vercel'
3+
import { databaseSchemaAgent } from './agents/databaseSchemaAgent'
4+
/**
5+
* Mastra instance with Langfuse tracing integration
6+
*
7+
* The telemetry configuration is set up to work with the Langfuse exporter
8+
* configured in instrumentation.ts. The serviceName must be 'ai' to match
9+
* the ATTR_SERVICE_NAME in the NodeSDK configuration.
10+
*/
11+
export const mastra = new Mastra({
12+
agents: {
13+
databaseSchemaAgent,
14+
},
15+
telemetry: {
16+
serviceName: 'ai', // Must match ATTR_SERVICE_NAME in instrumentation.ts
17+
enabled: true,
18+
export: {
19+
type: 'custom',
20+
exporter: new LangfuseExporter({
21+
publicKey: process.env.LANGFUSE_PUBLIC_KEY || '',
22+
secretKey: process.env.LANGFUSE_SECRET_KEY || '',
23+
baseUrl: process.env.LANGFUSE_BASE_URL || 'https://cloud.langfuse.com',
24+
environment: process.env.NEXT_PUBLIC_ENV_NAME || 'development',
25+
debug: true,
26+
}),
27+
},
28+
},
29+
})

frontend/apps/app/libs/langfuse/langfuseHandler.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

frontend/apps/app/next.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ if (process.env.VERCEL_ENV === 'production') {
1313
}
1414

1515
const nextConfig: NextConfig = {
16+
// Server-only packages that should not be bundled on the client
17+
serverExternalPackages: ['@mastra/*'],
18+
1619
// NOTE: Exclude Prisma-related packages from the bundle
1720
// These packages are installed separately in the node_modules/@prisma directory
1821
// Excluding them prevents `Error: Cannot find module 'fs'` errors in the build process

frontend/apps/app/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"private": true,
44
"version": "0.1.0",
55
"dependencies": {
6+
"@ai-sdk/openai": "1.3.22",
67
"@codemirror/commands": "6.8.1",
78
"@codemirror/lang-json": "6.0.1",
89
"@codemirror/lang-yaml": "6.1.2",
@@ -11,15 +12,14 @@
1112
"@codemirror/merge": "6.10.0",
1213
"@codemirror/state": "6.5.2",
1314
"@codemirror/view": "6.36.6",
14-
"@langchain/core": "0.3.49",
15-
"@langchain/openai": "0.5.7",
1615
"@lezer/highlight": "1.2.1",
1716
"@liam-hq/db": "workspace:*",
1817
"@liam-hq/db-structure": "workspace:*",
1918
"@liam-hq/erd-core": "workspace:*",
2019
"@liam-hq/github": "workspace:*",
2120
"@liam-hq/jobs": "workspace:*",
2221
"@liam-hq/ui": "workspace:*",
22+
"@mastra/core": "0.9.3",
2323
"@next/third-parties": "15.3.1",
2424
"@octokit/auth-app": "7.2.1",
2525
"@octokit/rest": "21.1.1",
@@ -28,16 +28,17 @@
2828
"@trigger.dev/build": "3.3.17",
2929
"@trigger.dev/sdk": "3.3.17",
3030
"@types/react-syntax-highlighter": "15.5.13",
31+
"@vercel/otel": "1.12.0",
32+
"ai": "4.3.15",
3133
"cheerio": "1.0.0",
3234
"clsx": "2.1.1",
3335
"codemirror": "6.0.1",
3436
"date-fns": "4.1.0",
3537
"diff": "7.0.0",
3638
"dotenv": "16.5.0",
3739
"flags": "4.0.0",
38-
"langchain": "0.3.24",
3940
"langfuse": "3.37.2",
40-
"langfuse-langchain": "3.37.2",
41+
"langfuse-vercel": "3.37.2",
4142
"lucide-react": "0.503.0",
4243
"next": "15.3.1",
4344
"react": "18.3.1",

0 commit comments

Comments
 (0)