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
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Send } from '@langchain/langgraph'
import { convertSchemaToText } from '../../utils/convertSchemaToText'
import type { QaAgentState } from '../shared/qaAgentAnnotation'
import { getUnprocessedRequirements } from './getUnprocessedRequirements'

Expand All @@ -11,7 +10,6 @@ export type { RequirementData } from './types'
*/
export function continueToRequirements(state: QaAgentState) {
const targetRequirements = getUnprocessedRequirements(state)
const schemaContext = convertSchemaToText(state.schemaData)

// Use Send API to distribute each requirement for parallel processing
// Each requirement will be processed by testcaseGeneration with isolated state
Expand All @@ -20,7 +18,7 @@ export function continueToRequirements(state: QaAgentState) {
new Send('testcaseGeneration', {
// Each subgraph gets its own isolated state
currentRequirement: reqData,
schemaContext,
schemaData: state.schemaData,
messages: [], // Start with empty messages for isolation
testcases: [], // Will be populated by the subgraph
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import type { Testcase } from '../types'
*/
export const qaAgentAnnotation = Annotation.Root({
...MessagesAnnotation.spec,
schemaData: Annotation<Schema>,
schemaData: Annotation<Schema>({
// Read-only field: QA agent should not modify schema data, only read it
// Using identity reducer to maintain existing value and avoid InvalidUpdateError
// when multiple subgraphs pass the same schemaData back to parent state
reducer: (x) => x,
}),
analyzedRequirements: Annotation<AnalyzedRequirements>({
reducer: (x, y) => y ?? x,
default: () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AIMessage, type BaseMessage } from '@langchain/core/messages'
import { v4 as uuidv4 } from 'uuid'
import type { testcaseAnnotation } from './testcaseAnnotation'

export async function executeSingleTestNode(
_state: typeof testcaseAnnotation.State,
): Promise<{ messages: BaseMessage[] }> {
const toolCallId = uuidv4()
const aiMessage = new AIMessage({
content: 'Running single test case to validate the generated testcase.',
tool_calls: [
{
id: toolCallId,
name: 'runSingleTestTool',
args: {},
},
],
})

return { messages: [aiMessage] }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '@langchain/core/messages'
import { ChatOpenAI } from '@langchain/openai'
import { fromAsyncThrowable } from '@liam-hq/neverthrow'
import { convertSchemaToText } from '../../utils/convertSchemaToText'
import { removeReasoningFromMessages } from '../../utils/messageCleanup'
import { streamLLMResponse } from '../../utils/streamingLlmUtils'
import { saveTestcaseTool } from '../tools/saveTestcaseTool'
Expand Down Expand Up @@ -32,7 +33,9 @@ const model = new ChatOpenAI({
export async function generateTestcaseNode(
state: typeof testcaseAnnotation.State,
): Promise<{ messages: BaseMessage[] }> {
const { currentRequirement, schemaContext, messages } = state
const { currentRequirement, schemaData, messages } = state

const schemaContext = convertSchemaToText(schemaData)

const contextMessage = await humanPromptTemplateForTestcaseGeneration.format({
schemaContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { END, START, StateGraph } from '@langchain/langgraph'
import { aColumn, aSchema, aTable } from '@liam-hq/schema'
import { describe, it } from 'vitest'
import {
getTestConfig,
Expand All @@ -20,6 +21,27 @@ describe('testcaseGeneration Integration', () => {

type TestcaseState = typeof testcaseAnnotation.State

const mockSchema = aSchema({
tables: {
users: aTable({
name: 'users',
columns: {
id: aColumn({ name: 'id', type: 'uuid', notNull: true }),
email: aColumn({ name: 'email', type: 'varchar', notNull: true }),
},
}),
tasks: aTable({
name: 'tasks',
columns: {
id: aColumn({ name: 'id', type: 'uuid', notNull: true }),
user_id: aColumn({ name: 'user_id', type: 'uuid', notNull: true }),
title: aColumn({ name: 'title', type: 'varchar', notNull: true }),
status: aColumn({ name: 'status', type: 'varchar', notNull: true }),
},
}),
},
})

const state: TestcaseState = {
messages: [],
currentRequirement: {
Expand All @@ -30,17 +52,7 @@ describe('testcaseGeneration Integration', () => {
'A task management system where users create projects and tasks',
requirementId: '550e8400-e29b-41d4-a716-446655440000',
},
schemaContext: `
Table: users
- id: uuid (not null)
- email: varchar (not null)

Table: tasks
- id: uuid (not null)
- user_id: uuid (not null)
- title: varchar (not null)
- status: varchar (not null)
`,
schemaData: mockSchema,
testcases: [],
schemaIssues: [],
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { END, START, StateGraph } from '@langchain/langgraph'
import { RETRY_POLICY } from '../../utils/errorHandling'
import { executeSingleTestNode } from './executeSingleTestNode'
import { generateTestcaseNode } from './generateTestcaseNode'
import { invokeSingleTestToolNode } from './invokeSingleTestToolNode'
import { routeAfterGenerate } from './routeAfterGenerate'
import { routeAfterSave } from './routeAfterSave'
import { saveToolNode } from './saveToolNode'
Expand All @@ -20,14 +22,23 @@ graph
.addNode('invokeSaveTool', saveToolNode, {
retryPolicy: RETRY_POLICY,
})
.addNode('executeSingleTest', executeSingleTestNode, {
retryPolicy: RETRY_POLICY,
})
.addNode('invokeSingleTestTool', invokeSingleTestToolNode, {
retryPolicy: RETRY_POLICY,
})
.addEdge(START, 'validateSchemaRequirements')
.addConditionalEdges('generateTestcase', routeAfterGenerate, {
invokeSaveTool: 'invokeSaveTool',
[END]: END,
})
.addConditionalEdges('invokeSaveTool', routeAfterSave, {
generateTestcase: 'generateTestcase',
executeSingleTest: 'executeSingleTest',
[END]: END,
})
.addEdge('executeSingleTest', 'invokeSingleTestTool')
.addEdge('invokeSingleTestTool', END)

export const testcaseGeneration = graph.compile()
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { RunnableConfig } from '@langchain/core/runnables'
import { ToolNode } from '@langchain/langgraph/prebuilt'
import { runSingleTestTool } from '../../tools/runSingleTestTool'
import { generateDdlFromSchema } from '../../utils/generateDdl'
import type { testcaseAnnotation } from './testcaseAnnotation'

const toolNode = new ToolNode([runSingleTestTool])

export const invokeSingleTestToolNode = async (
state: typeof testcaseAnnotation.State,
config: RunnableConfig,
): Promise<Partial<typeof testcaseAnnotation.State>> => {
const ddlStatements = generateDdlFromSchema(state.schemaData)
const requiredExtensions = Object.keys(state.schemaData.extensions).sort()

const enhancedConfig: RunnableConfig = {
...config,
configurable: {
...config.configurable,
testcases: state.testcases,
ddlStatements,
requiredExtensions,
},
}

return await toolNode.invoke(state, enhancedConfig)
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { END } from '@langchain/langgraph'
import type { END } from '@langchain/langgraph'
import type { testcaseAnnotation } from './testcaseAnnotation'

/**
* Route after saveToolNode based on whether testcases were successfully saved
*/
export const routeAfterSave = (
state: typeof testcaseAnnotation.State,
): 'generateTestcase' | typeof END => {
): 'generateTestcase' | 'executeSingleTest' | typeof END => {
const { testcases } = state

if (testcases.length > 0) {
return END
return 'executeSingleTest'
}

return 'generateTestcase'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Annotation, MessagesAnnotation } from '@langchain/langgraph'
import type { Schema } from '@liam-hq/schema'
import type { RequirementData } from '../distributeRequirements'
import type { Testcase } from '../types'

Expand All @@ -15,7 +16,7 @@ export const schemaIssuesAnnotation = Annotation<Array<SchemaIssue>>({
export const testcaseAnnotation = Annotation.Root({
...MessagesAnnotation.spec,
currentRequirement: Annotation<RequirementData>,
schemaContext: Annotation<string>,
schemaData: Annotation<Schema>,
testcases: Annotation<Testcase[]>({
reducer: (prev, next) => prev.concat(next),
}),
Expand Down
Loading
Loading