generated from JetBrains/intellij-platform-plugin-template
-
Notifications
You must be signed in to change notification settings - Fork 473
feat(chatdb): add ChatDB agent for text-to-SQL interactions #509
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
eb0d669
feat(chatdb): add ChatDB agent for text-to-SQL interactions
phodal 2bd950b
refactor(agent): simplify agent type handling and ChatDB workspace
phodal 277aff6
feat(ui): conditionally show SessionSidebar by agent type
phodal 7b25d35
feat(chatdb): add ChatDB agent and CLI support #508
phodal 16c9cc0
feat(chatdb): add SqlReviseAgent SubAgent and tests
phodal 56c1925
fix: resolve CI build failures for mpp-core, mpp-ui, and mpp-idea
phodal b383402
fix: add missing CHAT_DB branch and iOS actual implementation
phodal d295e09
feat(chatdb): add LLM-based schema linker and SQL validation
phodal bb4b2e3
refactor(agent): clarify SQL prompt instructions and output #508
phodal 92def0e
feat(chatdb): improve schema linking with sample data #508
phodal 61ecd4f
fix(chatdb): ensure single SQL statement extraction #508
phodal 681dd27
refactor(chatdb): move KeywordSchemaLinker to separate file #508
phodal 03d891b
feat(chatdb): add platform-specific NLP keyword extraction #508
phodal 143421a
refactor(nlp): replace FallbackTokenizer with FallbackNlpTokenizer
phodal 8268476
refactor(chatdb): remove SqlRevisionContext data class #508
phodal 038bd93
test(chatdb): add tests for FallbackNlpTokenizer #508
phodal b32681b
feat(chatdb): add inline data source config pane #508
phodal a4eacbd
refactor(agent): migrate SQL validator to multiplatform #508
phodal 668ab07
refactor(chatdb): remove DataSourceConfigDialog component #508
phodal fe33fe5
feat(chatdb): add persistent data source repository #508
phodal d7a8f04
refactor(chatdb): remove unused SchemaInfoDialog component #508
phodal a9ad4f4
feat(chatdb): render successful query results as messages #508
phodal b8b3ce2
feat(chatdb): render agent progress and results in timeline #508
phodal 9515c49
feat(chatdb): enhance LLM response rendering with details #508
phodal 40671d5
feat(chatdb): add interactive execution step cards to UI #508
phodal e60a1ed
feat(chatdb): add new session button to chat pane
phodal 5f74492
feat(chatdb): add table schema and query preview UI #508
phodal 755db21
feat(chatdb): add multi-database chat agent support #508
phodal 97561fe
feat(chatdb): add SQL validation, revision, and visualization #508
phodal 97c1793
feat(chatdb): add PlotDSLAgent for query visualizations #508
phodal 7e62448
feat(database): output query results as Markdown tables #508
phodal 017ddfe
feat(chatdb): refactor multi-database agent and renderer #508
phodal 99037b0
feat(renderer): refactor agent and database rendering logic #508
phodal 55a5713
feat(chatdb): render success for revised dry run validation #508
phodal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
mpp-core/src/androidMain/kotlin/cc/unitmesh/agent/chatdb/NlpTokenizer.android.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package cc.unitmesh.agent.chatdb | ||
|
|
||
| /** | ||
| * Android implementation of NlpTokenizer. | ||
| * Uses the fallback regex-based tokenization since MyNLP is JVM-only | ||
| * and may have compatibility issues on Android. | ||
| * | ||
| * TODO: Consider using Android's BreakIterator or a lightweight NLP library for better tokenization. | ||
| */ | ||
| actual object NlpTokenizer { | ||
| /** | ||
| * Extract keywords from natural language query using simple tokenization. | ||
| * Supports both English and Chinese text. | ||
| * | ||
| * @param query The natural language query to tokenize | ||
| * @param stopWords Set of words to filter out from results | ||
| * @return List of extracted keywords | ||
| */ | ||
| actual fun extractKeywords(query: String, stopWords: Set<String>): List<String> { | ||
| return FallbackNlpTokenizer.extractKeywords(query, stopWords) | ||
| } | ||
| } | ||
|
|
125 changes: 125 additions & 0 deletions
125
mpp-core/src/androidMain/kotlin/cc/unitmesh/agent/subagent/SqlValidator.android.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package cc.unitmesh.agent.subagent | ||
|
|
||
| import net.sf.jsqlparser.parser.CCJSqlParserUtil | ||
| import net.sf.jsqlparser.statement.Statement | ||
| import net.sf.jsqlparser.util.TablesNamesFinder | ||
|
|
||
| /** | ||
| * Android implementation of SqlValidator using JSqlParser. | ||
| * | ||
| * This validator uses JSqlParser to validate SQL syntax. | ||
| * It can detect: | ||
| * - Syntax errors | ||
| * - Malformed SQL statements | ||
| * - Unsupported SQL constructs | ||
| * - Table names not in whitelist (schema validation) | ||
| */ | ||
| actual class SqlValidator actual constructor() : SqlValidatorInterface { | ||
|
|
||
| actual override fun validate(sql: String): SqlValidationResult { | ||
| return try { | ||
| val statement: Statement = CCJSqlParserUtil.parse(sql) | ||
| SqlValidationResult( | ||
| isValid = true, | ||
| errors = emptyList(), | ||
| warnings = collectWarnings(statement) | ||
| ) | ||
| } catch (e: Exception) { | ||
| SqlValidationResult( | ||
| isValid = false, | ||
| errors = listOf(extractErrorMessage(e)), | ||
| warnings = emptyList() | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| actual override fun validateWithTableWhitelist(sql: String, allowedTables: Set<String>): SqlValidationResult { | ||
| return try { | ||
| val statement: Statement = CCJSqlParserUtil.parse(sql) | ||
|
|
||
| // Extract table names from the SQL | ||
| val tablesNamesFinder = TablesNamesFinder() | ||
| val usedTables = tablesNamesFinder.getTableList(statement) | ||
|
|
||
| // Check if all used tables are in the whitelist (case-insensitive) | ||
| val allowedTablesLower = allowedTables.map { it.lowercase() }.toSet() | ||
| val invalidTables = usedTables.filter { tableName -> | ||
| tableName.lowercase() !in allowedTablesLower | ||
| } | ||
|
|
||
| if (invalidTables.isNotEmpty()) { | ||
| SqlValidationResult( | ||
| isValid = false, | ||
| errors = listOf( | ||
| "Invalid table(s) used: ${invalidTables.joinToString(", ")}. " + | ||
| "Available tables: ${allowedTables.joinToString(", ")}" | ||
| ), | ||
| warnings = collectWarnings(statement) | ||
| ) | ||
| } else { | ||
| SqlValidationResult( | ||
| isValid = true, | ||
| errors = emptyList(), | ||
| warnings = collectWarnings(statement) | ||
| ) | ||
| } | ||
| } catch (e: Exception) { | ||
| SqlValidationResult( | ||
| isValid = false, | ||
| errors = listOf(extractErrorMessage(e)), | ||
| warnings = emptyList() | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| actual override fun extractTableNames(sql: String): List<String> { | ||
| return try { | ||
| val statement: Statement = CCJSqlParserUtil.parse(sql) | ||
| val tablesNamesFinder = TablesNamesFinder() | ||
| tablesNamesFinder.getTableList(statement) | ||
| } catch (e: Exception) { | ||
| emptyList() | ||
| } | ||
| } | ||
|
|
||
| private fun extractErrorMessage(e: Exception): String { | ||
| val message = e.message ?: "Unknown SQL parsing error" | ||
| return when { | ||
| message.contains("Encountered") -> { | ||
| val match = Regex("Encountered \"(.+?)\" at line (\\d+), column (\\d+)").find(message) | ||
| if (match != null) { | ||
| val (token, line, column) = match.destructured | ||
| "Syntax error at line $line, column $column: unexpected token '$token'" | ||
| } else { | ||
| message | ||
| } | ||
| } | ||
| message.contains("Was expecting") -> { | ||
| val match = Regex("Was expecting.*?:\\s*(.+)").find(message) | ||
| if (match != null) { | ||
| "Expected: ${match.groupValues[1].take(100)}" | ||
| } else { | ||
| message | ||
| } | ||
| } | ||
| else -> message.take(200) | ||
| } | ||
| } | ||
|
|
||
| private fun collectWarnings(statement: Statement): List<String> { | ||
| val warnings = mutableListOf<String>() | ||
| val sql = statement.toString() | ||
|
|
||
| if (sql.contains("SELECT *")) { | ||
| warnings.add("Consider specifying explicit columns instead of SELECT *") | ||
| } | ||
|
|
||
| if (!sql.contains("WHERE", ignoreCase = true) && | ||
| (sql.contains("UPDATE", ignoreCase = true) || sql.contains("DELETE", ignoreCase = true))) { | ||
| warnings.add("UPDATE/DELETE without WHERE clause will affect all rows") | ||
| } | ||
|
|
||
| return warnings | ||
| } | ||
| } | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
172 changes: 172 additions & 0 deletions
172
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/chatdb/ChatDBAgent.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| package cc.unitmesh.agent.chatdb | ||
|
|
||
| import cc.unitmesh.agent.config.McpToolConfigService | ||
| import cc.unitmesh.agent.core.MainAgent | ||
| import cc.unitmesh.agent.database.DatabaseConfig | ||
| import cc.unitmesh.agent.database.DatabaseConnection | ||
| import cc.unitmesh.agent.database.createDatabaseConnection | ||
| import cc.unitmesh.agent.logging.getLogger | ||
| import cc.unitmesh.agent.model.AgentDefinition | ||
| import cc.unitmesh.agent.model.PromptConfig | ||
| import cc.unitmesh.agent.model.RunConfig | ||
| import cc.unitmesh.agent.orchestrator.ToolOrchestrator | ||
| import cc.unitmesh.agent.policy.DefaultPolicyEngine | ||
| import cc.unitmesh.agent.render.CodingAgentRenderer | ||
| import cc.unitmesh.agent.render.DefaultCodingAgentRenderer | ||
| import cc.unitmesh.agent.tool.shell.DefaultShellExecutor | ||
| import cc.unitmesh.agent.tool.shell.ShellExecutor | ||
| import cc.unitmesh.agent.tool.ToolResult | ||
| import cc.unitmesh.agent.tool.filesystem.DefaultToolFileSystem | ||
| import cc.unitmesh.agent.tool.filesystem.ToolFileSystem | ||
| import cc.unitmesh.agent.tool.registry.ToolRegistry | ||
| import cc.unitmesh.llm.KoogLLMService | ||
| import cc.unitmesh.llm.ModelConfig | ||
|
|
||
| /** | ||
| * ChatDB Agent - Text2SQL Agent for natural language database queries | ||
| * | ||
| * This agent converts natural language queries to SQL, executes them, | ||
| * and optionally generates visualizations of the results. | ||
| * | ||
| * Features: | ||
| * - Schema Linking: Keyword-based search to find relevant tables/columns | ||
| * - SQL Generation: LLM generates SQL from natural language | ||
| * - Revise Agent: Self-correction loop using JSqlParser for SQL validation | ||
| * - Query Execution: Execute validated SQL and return results | ||
| * - Visualization: Optional PlotDSL generation for data visualization | ||
| * | ||
| * Based on GitHub Issue #508: https://github.com/phodal/auto-dev/issues/508 | ||
| */ | ||
| class ChatDBAgent( | ||
| private val projectPath: String, | ||
| private val llmService: KoogLLMService, | ||
| private val databaseConfig: DatabaseConfig, | ||
| override val maxIterations: Int = 10, | ||
| private val renderer: CodingAgentRenderer = DefaultCodingAgentRenderer(), | ||
| private val fileSystem: ToolFileSystem? = null, | ||
| private val shellExecutor: ShellExecutor? = null, | ||
| private val mcpToolConfigService: McpToolConfigService, | ||
| private val enableLLMStreaming: Boolean = true | ||
| ) : MainAgent<ChatDBTask, ToolResult.AgentResult>( | ||
| AgentDefinition( | ||
| name = "ChatDBAgent", | ||
| displayName = "ChatDB Agent", | ||
| description = "Text2SQL Agent that converts natural language to SQL queries with schema linking and self-correction", | ||
| promptConfig = PromptConfig( | ||
| systemPrompt = SYSTEM_PROMPT | ||
| ), | ||
| modelConfig = ModelConfig.default(), | ||
| runConfig = RunConfig(maxTurns = 10, maxTimeMinutes = 5) | ||
| ) | ||
| ) { | ||
| private val logger = getLogger("ChatDBAgent") | ||
|
|
||
| private val actualFileSystem = fileSystem ?: DefaultToolFileSystem(projectPath = projectPath) | ||
|
|
||
| private val toolRegistry = ToolRegistry( | ||
| fileSystem = actualFileSystem, | ||
| shellExecutor = shellExecutor ?: DefaultShellExecutor(), | ||
| configService = mcpToolConfigService, | ||
| llmService = llmService | ||
| ) | ||
|
|
||
| private val policyEngine = DefaultPolicyEngine() | ||
|
|
||
| private val toolOrchestrator = ToolOrchestrator( | ||
| registry = toolRegistry, | ||
| policyEngine = policyEngine, | ||
| renderer = renderer, | ||
| mcpConfigService = mcpToolConfigService | ||
| ) | ||
|
|
||
| private var databaseConnection: DatabaseConnection? = null | ||
|
|
||
| private val executor: ChatDBAgentExecutor by lazy { | ||
| val connection = databaseConnection ?: createDatabaseConnection(databaseConfig) | ||
| databaseConnection = connection | ||
|
|
||
| ChatDBAgentExecutor( | ||
| projectPath = projectPath, | ||
| llmService = llmService, | ||
| toolOrchestrator = toolOrchestrator, | ||
| renderer = renderer, | ||
| databaseConnection = connection, | ||
| maxIterations = maxIterations, | ||
| enableLLMStreaming = enableLLMStreaming | ||
| ) | ||
| } | ||
|
|
||
| override fun validateInput(input: Map<String, Any>): ChatDBTask { | ||
| val query = input["query"] as? String | ||
| ?: throw IllegalArgumentException("Missing required parameter: query") | ||
|
|
||
| return ChatDBTask( | ||
| query = query, | ||
| additionalContext = input["additionalContext"] as? String ?: "", | ||
| maxRows = (input["maxRows"] as? Number)?.toInt() ?: 100, | ||
| generateVisualization = input["generateVisualization"] as? Boolean ?: true | ||
| ) | ||
| } | ||
|
|
||
| override suspend fun execute( | ||
| input: ChatDBTask, | ||
| onProgress: (String) -> Unit | ||
| ): ToolResult.AgentResult { | ||
| logger.info { "Starting ChatDB Agent for query: ${input.query}" } | ||
|
|
||
| val systemPrompt = buildSystemPrompt() | ||
| val result = executor.execute(input, systemPrompt, onProgress) | ||
|
|
||
| return ToolResult.AgentResult( | ||
| success = result.success, | ||
| content = result.message, | ||
| metadata = mapOf( | ||
| "generatedSql" to (result.generatedSql ?: ""), | ||
| "rowCount" to (result.queryResult?.rowCount?.toString() ?: "0"), | ||
| "revisionAttempts" to result.revisionAttempts.toString(), | ||
| "hasVisualization" to (result.plotDslCode != null).toString() | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| private fun buildSystemPrompt(): String { | ||
| return SYSTEM_PROMPT | ||
| } | ||
|
|
||
| override fun formatOutput(output: ToolResult.AgentResult): String { | ||
| return output.content | ||
| } | ||
|
|
||
| override fun getParameterClass(): String = "ChatDBTask" | ||
|
|
||
| /** | ||
| * Close database connection when done | ||
| */ | ||
| suspend fun close() { | ||
| databaseConnection?.close() | ||
| databaseConnection = null | ||
| } | ||
|
|
||
| companion object { | ||
| const val SYSTEM_PROMPT = """You are an expert SQL developer. Generate SQL queries from natural language. | ||
|
|
||
| CRITICAL RULES - YOU MUST FOLLOW THESE: | ||
| 1. ONLY use table names provided in the schema - NEVER invent or guess table names | ||
| 2. ONLY use column names provided in the schema - NEVER invent or guess column names | ||
| 3. If a table or column doesn't exist in the schema, DO NOT use it | ||
| 4. Only generate SELECT queries (read-only operations) | ||
| 5. Always add LIMIT clause to prevent large result sets | ||
|
|
||
| OUTPUT FORMAT: | ||
| - Return ONLY the SQL query wrapped in ```sql code block | ||
| - Do NOT include explanations, alternatives, or reasoning | ||
| - Do NOT add comments outside the code block | ||
| - Keep response concise - just the SQL | ||
|
|
||
| Example response: | ||
| ```sql | ||
| SELECT id, name FROM users WHERE status = 'active' LIMIT 100; | ||
| ```""" | ||
| } | ||
| } | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding error handling for connection creation.
Similar to
MultiDatabaseChatDBAgent, the lazy initialization ofexecutorcreates a database connection without error handling. IfcreateDatabaseConnectionfails, the agent becomes unusable.private val executor: ChatDBAgentExecutor by lazy { - val connection = databaseConnection ?: createDatabaseConnection(databaseConfig) - databaseConnection = connection + val connection = databaseConnection ?: try { + createDatabaseConnection(databaseConfig) + } catch (e: Exception) { + logger.error { "Failed to create database connection: ${e.message}" } + throw IllegalStateException("Cannot create database connection", e) + } + databaseConnection = connection ChatDBAgentExecutor(🤖 Prompt for AI Agents