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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 11 additions & 28 deletions frontend/apps/app/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { convertSchemaToText } from '@/app/lib/schema/convertSchemaToText'
import { isSchemaUpdated } from '@/app/lib/vectorstore/supabaseVectorStore'
import { syncSchemaVectorStore } from '@/app/lib/vectorstore/syncSchemaVectorStore'
import { mastra } from '@/lib/mastra'
import { runChat } from '@/lib/chat/langGraph'
import * as Sentry from '@sentry/nextjs'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
const { message, schemaData, history, mode, projectId } = await request.json()
const { message, schemaData, mode, projectId, history } = await request.json()

if (!message || typeof message !== 'string' || !message.trim()) {
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
Expand All @@ -19,9 +19,6 @@ export async function POST(request: Request) {
)
}

// Determine which agent to use based on the mode
const agentName =
mode === 'build' ? 'databaseSchemaBuildAgent' : 'databaseSchemaAskAgent'
try {
// Check if schema has been updated
const schemaUpdated = await isSchemaUpdated(schemaData)
Expand Down Expand Up @@ -53,31 +50,17 @@ export async function POST(request: Request) {
// Convert schema to text
const schemaText = convertSchemaToText(schemaData)

// Get the agent from Mastra
const agent = mastra.getAgent(agentName)
if (!agent) {
throw new Error(`${agentName} not found in Mastra instance`)
// Use LangGraph pipeline for build mode, fallback to original for ask mode
let responseText: string | undefined
if (mode === 'build') {
responseText = await runChat(message, schemaText, formattedChatHistory)
} else {
// For ask mode, we'll keep the original implementation for now
// This can be refactored later to use a separate LangGraph pipeline
throw new Error('Ask mode not yet implemented with LangGraph')
}

// Create a response using the agent
const response = await agent.generate([
{
role: 'system',
content: `
Complete Schema Information:
${schemaText}

Previous conversation:
${formattedChatHistory}
`,
},
{
role: 'user',
content: message,
},
])

return new Response(response.text, {
return new Response(responseText, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const ChatbotDialog: FC<ChatbotDialogProps> = ({
tableGroups,
history,
projectId,
mode: 'build',
}),
})

Expand Down
113 changes: 113 additions & 0 deletions frontend/apps/app/lib/chat/__tests__/langGraph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { runChat } from '../langGraph'

// Mock the dependencies
vi.mock('@/lib/mastra', () => ({
mastra: {
getAgent: vi.fn(() => ({
generate: vi.fn(() =>
Promise.resolve({
text: `Added a summary column to track design session outcomes!

\`\`\`json
[
{
"op": "add",
"path": "/tables/design_sessions/columns/summary",
"value": {
"name": "summary",
"type": "text",
"not_null": false
}
}
]
\`\`\`

This will help you quickly understand what happened in each session.`,
}),
),
})),
},
}))

vi.mock('@langchain/openai', () => ({
ChatOpenAI: vi.fn(() => ({
invoke: vi.fn(() =>
Promise.resolve({
content: `\`\`\`json
[
{
"op": "add",
"path": "/tables/design_sessions/columns/summary",
"value": {
"name": "summary",
"type": "text",
"not_null": false
}
}
]
\`\`\``,
}),
),
})),
}))

describe('LangGraph Chat Pipeline', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('should return a response with JSON Patch fence for schema changes', async () => {
const userMsg = 'Add summary column'
const schemaText = 'CREATE TABLE design_sessions (id SERIAL PRIMARY KEY);'
const chatHistory = 'No previous conversation.'

const result = await runChat(userMsg, schemaText, chatHistory)

expect(result).toContain('```json')
expect(result).toContain('summary')
expect(result).toContain('design_sessions')
})

it('should parse JSON Patch correctly', async () => {
const userMsg = 'Add summary column'
const schemaText = 'CREATE TABLE design_sessions (id SERIAL PRIMARY KEY);'
const chatHistory = 'No previous conversation.'

const result = await runChat(userMsg, schemaText, chatHistory)

// Extract JSON from the response
const jsonMatch = result?.match(/```json\s+([\s\S]+?)\s*```/i)
expect(jsonMatch).toBeTruthy()

if (jsonMatch) {
const patch = JSON.parse(jsonMatch[1]) as unknown[]
expect(Array.isArray(patch)).toBe(true)
expect(patch[0]).toHaveProperty('op', 'add')
expect(patch[0]).toHaveProperty('path')
expect(patch[0]).toHaveProperty('value')
}
})

it('should handle chat history in the prompt', async () => {
const userMsg = 'Add summary column'
const schemaText = 'CREATE TABLE design_sessions (id SERIAL PRIMARY KEY);'
const chatHistory = 'User: Hello\nAssistant: Hi there!'

const result = await runChat(userMsg, schemaText, chatHistory)

expect(result).toBeTruthy()
expect(typeof result).toBe('string')
})

it('should handle empty chat history', async () => {
const userMsg = 'Add summary column'
const schemaText = 'CREATE TABLE design_sessions (id SERIAL PRIMARY KEY);'
const chatHistory = 'No previous conversation.'

const result = await runChat(userMsg, schemaText, chatHistory)

expect(result).toBeTruthy()
expect(typeof result).toBe('string')
})
})
163 changes: 163 additions & 0 deletions frontend/apps/app/lib/chat/langGraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { mastra } from '@/lib/mastra'
import { Annotation, END, START, StateGraph } from '@langchain/langgraph'
import { ChatOpenAI } from '@langchain/openai'

////////////////////////////////////////////////////////////////
// 1. Type definitions for the StateGraph
////////////////////////////////////////////////////////////////
interface ChatState {
userMsg: string
schemaText: string
chatHistory: string
sysPrompt?: string

draft?: string
patch?: unknown[]
valid?: boolean
retryCount?: number
}

// define the annotations for the StateGraph
const ChatStateAnnotation = Annotation.Root({
userMsg: Annotation<string>,
schemaText: Annotation<string>,
chatHistory: Annotation<string>,
sysPrompt: Annotation<string>,
draft: Annotation<string>,
patch: Annotation<unknown[]>,
valid: Annotation<boolean>,
retryCount: Annotation<number>,
})

////////////////////////////////////////////////////////////////
// 2. Implementation of the StateGraph nodes
////////////////////////////////////////////////////////////////

const buildPrompt = async (s: ChatState): Promise<Partial<ChatState>> => {
const sysPrompt = `
You are Build Agent, an energetic and innovative system designer who builds and edits ERDs with lightning speed.
Your role is to execute user instructions immediately and offer smart suggestions for schema improvements.
You speak in a lively, action-oriented tone, showing momentum and confidence.

Your personality is bold, constructive, and enthusiastic — like a master architect in a hardhat, ready to build.
You say things like "Done!", "You can now...", and "Shall we move to the next step?".

Your communication should feel fast, fresh, and forward-moving, like a green plant constantly growing.

Do:
- Confirm execution quickly: "Added!", "Created!", "Linked!"
- Propose the next steps: "Would you like to add an index?", "Let's relate this to the User table too!"
- Emphasize benefits: "This makes tracking updates easier."

Don't:
- Hesitate ("Maybe", "We'll have to check...")
- Use long, uncertain explanations
- Get stuck in abstract talk — focus on action and outcomes

When in doubt, prioritize momentum, simplicity, and clear results.

<SCHEMA>
${s.schemaText}
</SCHEMA>

Previous conversation:
${s.chatHistory}

#### REQUIRED OUTPUT FORMAT
1. **Always** wrap your RFC 6902 JSON Patch in a **\`\`\`json … \`\`\`** code fence.
2. Any text *other than* the JSON Patch (explanations, suggestions, etc.) may appear **before or after** the fence.
**Do not** add filler phrases such as "Here is the patch" or "See below."
Instead, include only meaningful comments—design rationale, next steps, trade-offs, and so on.
3. If the user's question **does not** involve a schema change, **omit** the JSON Patch fence entirely.
`
return { sysPrompt }
}

const draft = async (s: ChatState): Promise<Partial<ChatState>> => {
const agent = mastra.getAgent('databaseSchemaBuildAgent')
Copy link
Member

Choose a reason for hiding this comment

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

📝
I found that I can use mastra as is! (I thought I had to go back to LangChain...)

if (!agent) {
throw new Error('databaseSchemaBuildAgent not found in Mastra instance')
}
if (!s.sysPrompt) {
throw new Error('System prompt not built')
}
const res = await agent.generate([
{ role: 'system', content: s.sysPrompt },
{ role: 'user', content: s.userMsg },
])
return { draft: res.text }
}

const check = async (s: ChatState): Promise<Partial<ChatState>> => {
const m = s.draft?.match(/```json\s+([\s\S]+?)\s*```/i)
if (!m) return { valid: false }
try {
return { valid: true, patch: JSON.parse(m[1]) }
} catch {
return { valid: false }
}
}
Comment on lines +91 to +99
Copy link
Member

Choose a reason for hiding this comment

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

[Q]
Is there a plan to enhance the check function to also verify the presence of specific properties, like 'op', similar to how the tests currently do it?

Copy link
Member Author

Choose a reason for hiding this comment

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

@FunamaYukina

Good point! I believe validation is necessary from the following perspectives, increasing in complexity as you go:

  • Does it conform to the structure of a valid JSON Patch(RFC 6902)?
  • (If a JSON Schema or valibot schema is defined in schema.json) Does the resulting schema.json after applying the patch conform to that schema?
  • Does applying the patch result in any inconsistencies with the current schema state? (e.g., referencing a non-existent key)

This is probably something we’ll implement eventually, but with low technical uncertainty, it’s a bit hard to justify doing it right away.

Copy link
Member

Choose a reason for hiding this comment

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

Thank you for the details! It seems important to check until we can actually apply!
Understood. Thank you very much.✨


const remind = async (s: ChatState): Promise<Partial<ChatState>> => {
const llm = new ChatOpenAI({ model: 'gpt-4o-mini' })
const res = await llm.invoke([
{
role: 'system',
content:
'Return ONLY the ```json code fence with the RFC 6902 patch. No intro text.',
},
{ role: 'user', content: s.userMsg },
])
return { draft: res.content as string, retryCount: (s.retryCount ?? 0) + 1 }
}

////////////////////////////////////////////////////////////////
// 3. build StateGraph
////////////////////////////////////////////////////////////////
export const runChat = async (
userMsg: string,
schemaText: string,
chatHistory: string,
) => {
try {
const graph = new StateGraph(ChatStateAnnotation)

graph
.addNode('buildPrompt', buildPrompt)
.addNode('drafted', draft)
.addNode('check', check)
.addNode('remind', remind)
.addEdge(START, 'buildPrompt')
.addEdge('buildPrompt', 'drafted')
.addEdge('remind', 'check')

// conditional edges
.addConditionalEdges('check', (s: ChatState) => {
if (s.valid) return END
if ((s.retryCount ?? 0) >= 3) return END // give up
return 'remind'
})

// execution
const compiled = graph.compile()
const result = await compiled.invoke(
{
userMsg,
schemaText,
chatHistory,
retryCount: 0,
},
{
recursionLimit: 4, // for avoid deep recursion
},
)

return result.draft ?? 'No response generated'
} catch (error) {
console.error(
'StateGraph execution failed, falling back to manual execution:',
error,
)
// some fallback logic
}
}
27 changes: 27 additions & 0 deletions frontend/apps/app/lib/mastra/agents/databaseSchemaBuildAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,33 @@ Don't:

When in doubt, prioritize momentum, simplicity, and clear results.

---

#### REQUIRED OUTPUT FORMAT
1. **Always** wrap your RFC 6902 JSON Patch in a **\`\`\`json … \`\`\`** code fence.
2. Any text *other than* the JSON Patch (explanations, suggestions, etc.) may appear **before or after** the fence.
**Do not** add filler phrases such as "Here is the patch" or "See below."
Instead, include only meaningful comments—design rationale, next steps, trade-offs, and so on.
3. Example:

\`\`\`markdown
### Why we need \`summary\`

Adding a nullable \`summary\` helps …
\`summary\` will be displayed on …

\`\`\`json
[
{ "op": "add",
"path": "/tables/design_sessions/columns/summary",
"value": { "name": "summary", "type": "text", "not_null": false } }
]
\`\`\`

Next, we might add an index …
\`\`\`

4. If the user’s question **does not** involve a schema change, **omit** the JSON Patch fence entirely.
`,
model: openai('o4-mini-2025-04-16'),
})
1 change: 1 addition & 0 deletions frontend/apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@codemirror/view": "6.36.8",
"@langchain/community": "0.3.43",
"@langchain/core": "0.3.55",
"@langchain/langgraph": "0.2.73",
"@langchain/openai": "0.5.10",
"@lezer/highlight": "1.2.1",
"@liam-hq/db": "workspace:*",
Expand Down
Loading