Spaces:
Runtime error
Runtime error
feat: synchronize text-to-sql-bot codebase with Hugging Face Space repository, including Docker build configurations
6086e71 | """ | |
| SQL Generation Agent — Generates SQL queries from natural language using LLM. | |
| Receives schema context from RAG and produces structured SQL output. | |
| """ | |
| import json | |
| import re | |
| import structlog | |
| from app.agents.state import AgentState | |
| from app.prompts.registry import get_prompt_registry | |
| logger = structlog.get_logger() | |
| def sql_generation_node(state: AgentState, llm_router) -> dict: | |
| """ | |
| Generate SQL query from the user's question using schema context. | |
| Outputs structured JSON with sql, explanation, and friendly message. | |
| """ | |
| user_query = state["user_query"] | |
| context = state.get("relevant_schema", "") | |
| history = state.get("conversation_history", []) | |
| _intent = state.get("intent", "data_query") | |
| retry_count = state.get("retry_count", 0) | |
| validation_errors = state.get("validation_errors", []) | |
| trace_id = state.get("trace_id", "unknown") | |
| logger.info("agent_started", agent="sql_generation", trace_id=trace_id, retry=retry_count) | |
| # Build conversation history context | |
| history_text = "" | |
| if history: | |
| recent = history[-3:] # Last 3 exchanges | |
| history_text = "PREVIOUS CONVERSATION:\n" | |
| for h in recent: | |
| history_text += f"User: {h.get('user', '')}\nSQL: {h.get('sql', '')}\n" | |
| # If this is a retry, include the validation errors for self-correction | |
| retry_context = "" | |
| if retry_count > 0 and validation_errors: | |
| retry_context = f""" | |
| ⚠️ YOUR PREVIOUS SQL WAS REJECTED. Fix these issues: | |
| {chr(10).join(f' - {err}' for err in validation_errors)} | |
| Previous attempt: {state.get('generated_sql', 'N/A')} | |
| Generate a corrected version. | |
| """ | |
| # Dynamic few-shot: select similar examples from eval dataset | |
| dynamic_examples = "" | |
| try: | |
| from app.prompts.few_shot import get_few_shot_selector | |
| selector = get_few_shot_selector() | |
| similar = selector.select(user_query, k=3) | |
| if similar: | |
| dynamic_examples = selector.format_for_prompt(similar) | |
| logger.debug("dynamic_few_shot_selected", count=len(similar)) | |
| except Exception as e: | |
| logger.debug("dynamic_few_shot_unavailable", error=str(e)) | |
| # Combine schema context with dynamic examples | |
| full_context = context | |
| if dynamic_examples: | |
| full_context = context + "\n" + dynamic_examples | |
| prompt_template = get_prompt_registry().get("sql_generation") | |
| prompt_version = prompt_template.version | |
| messages = prompt_template.render( | |
| schema_context=full_context, | |
| history_context=history_text, | |
| retry_context=retry_context, | |
| user_query=user_query, | |
| ) | |
| try: | |
| # Use higher quality model for complex queries | |
| model_pref = "accurate" if state.get("complexity") == "complex" else "default" | |
| response = llm_router.generate(messages, model_preference=model_pref, max_tokens=1024, temperature=0.1) | |
| # Parse structured response | |
| sql_query, explanation, message = _parse_llm_response(response) | |
| if not sql_query: | |
| logger.warning("empty_sql_generated", response_preview=response[:200]) | |
| return { | |
| "generated_sql": "", | |
| "sql_explanation": "Failed to generate SQL", | |
| "friendly_message": "I couldn't generate a query for that request. Could you rephrase?", | |
| "error": "Empty SQL output from LLM", | |
| "error_agent": "sql_generation", | |
| "prompt_version": prompt_version, | |
| } | |
| # Clean SQL | |
| sql_query = _clean_sql(sql_query) | |
| return { | |
| "generated_sql": sql_query, | |
| "sql_explanation": explanation, | |
| "friendly_message": message, | |
| "prompt_version": prompt_version, | |
| # Clear stale validation state from previous retry cycle. | |
| # Without this, LangGraph merges old is_valid=False into the | |
| # new state, causing route_validation to loop forever. | |
| "is_valid": None, | |
| "validation_errors": [], | |
| "sanitized_sql": "", | |
| } | |
| except Exception as e: | |
| logger.error("sql_generation_failed", error=str(e)) | |
| return { | |
| "generated_sql": "", | |
| "sql_explanation": "", | |
| "friendly_message": "An error occurred while generating the query.", | |
| "error": f"SQL generation failed: {str(e)}", | |
| "error_agent": "sql_generation", | |
| "prompt_version": prompt_version, | |
| } | |
| def _parse_llm_response(response: str) -> tuple[str, str, str]: | |
| """Parse the LLM response, handling both JSON and raw SQL formats.""" | |
| sql_query = "" | |
| explanation = "Query generated successfully." | |
| message = "Here are your results." | |
| try: | |
| # Try JSON parsing first | |
| clean_json = re.sub(r"```json|```", "", response).strip() | |
| data = json.loads(clean_json) | |
| sql_query = data.get("sql", "") | |
| message = data.get("message", message) | |
| explanation = data.get("explanation", explanation) | |
| except (json.JSONDecodeError, ValueError): | |
| # Fallback: extract SQL from raw text | |
| # Try to find SELECT...FROM...; (requires FROM to ensure it's SQL, not prose) | |
| match = re.search(r"((?:WITH\s+\w+\s+AS\s*\([\s\S]+?\)\s*)?SELECT\s+[\s\S]+?\sFROM\s[\s\S]+?;)", response, re.IGNORECASE) | |
| if match: | |
| sql_query = match.group(1) | |
| else: | |
| # Try without semicolon but still require FROM | |
| match = re.search(r"((?:WITH\s+\w+\s+AS\s*\([\s\S]+?\)\s*)?SELECT\s+[\s\S]+?\sFROM\s[\s\S]+?)(?:\n\n|$)", response, re.IGNORECASE) | |
| if match: | |
| sql_query = match.group(1) | |
| return sql_query, explanation, message | |
| def _clean_sql(sql: str) -> str: | |
| """Clean and normalize generated SQL.""" | |
| # Remove markdown formatting | |
| sql = re.sub(r"```sql|```", "", sql, flags=re.IGNORECASE).strip() | |
| # Normalize whitespace | |
| sql = " ".join(sql.split()) | |
| # Ensure trailing semicolon | |
| if sql and not sql.endswith(";"): | |
| sql += ";" | |
| return sql | |