Spaces:
Runtime error
Runtime error
feat: synchronize text-to-sql-bot codebase with Hugging Face Space repository, including Docker build configurations
6086e71 | """ | |
| Schema Retrieval Agent β Fetches relevant schema context using hybrid RAG. | |
| Uses both vector similarity and keyword search to find the best matching tables. | |
| retrieval_top_k is provided by the query_understanding agent and controls | |
| how many schema documents are fetched (3=simple, 5=default, 8=complex). | |
| """ | |
| import structlog | |
| from app.agents.state import AgentState | |
| logger = structlog.get_logger() | |
| # Valid top_k values β anything outside this range is clamped to the default. | |
| _VALID_TOP_K = frozenset({3, 5, 8}) | |
| _DEFAULT_TOP_K = 5 | |
| def _compress_schema_to_ddl(raw_schema: str) -> str: | |
| """ | |
| Compress verbose schema text into compact DDL-like format. | |
| Input format (from SchemaEnricher/get_full_schema): | |
| Table: employees | |
| Columns: | |
| - id (int) [PRIMARY KEY] | |
| - name (varchar(100)) | |
| - salary (decimal(10,2)) | |
| - department_id (int) [FOREIGN KEY] | |
| Relationships: | |
| - department_id β departments.id | |
| Output format: | |
| TABLE employees (id INT PK, name VARCHAR, salary DECIMAL, department_id INT FK) | |
| FK: employees.department_id β departments.id | |
| ~70% smaller, unambiguous column names for the LLM. | |
| """ | |
| import re | |
| lines = raw_schema.split("\n") | |
| tables = [] | |
| current_table = None | |
| columns = [] | |
| fks = [] | |
| for line in lines: | |
| stripped = line.strip() | |
| # Table header | |
| if stripped.startswith("Table: "): | |
| # Flush previous table | |
| if current_table and columns: | |
| tables.append(_format_ddl_table(current_table, columns, fks)) | |
| current_table = stripped.replace("Table: ", "").strip() | |
| columns = [] | |
| fks = [] | |
| # Column definition | |
| elif stripped.startswith("- ") and "(" in stripped: | |
| col_match = re.match( | |
| r'-\s+(\w+)\s+\(([^)]+)\)\s*(.*)', stripped | |
| ) | |
| if col_match: | |
| col_name = col_match.group(1) | |
| col_type = col_match.group(2).split("(")[0].upper() # INT, VARCHAR, etc. | |
| flags = col_match.group(3) | |
| suffix = "" | |
| if "PRIMARY KEY" in flags: | |
| suffix = " PK" | |
| elif "FOREIGN KEY" in flags: | |
| suffix = " FK" | |
| columns.append(f"{col_name} {col_type}{suffix}") | |
| # Relationship line | |
| elif "β" in stripped and current_table: | |
| fk_match = re.match(r'-?\s*(\w+)\s*β\s*(\w+)\.(\w+)', stripped) | |
| if fk_match: | |
| fks.append( | |
| f"{current_table}.{fk_match.group(1)} β {fk_match.group(2)}.{fk_match.group(3)}" | |
| ) | |
| # Flush last table | |
| if current_table and columns: | |
| tables.append(_format_ddl_table(current_table, columns, fks)) | |
| return "\n".join(tables) if tables else raw_schema # Fallback if parsing fails | |
| def _format_ddl_table(table_name: str, columns: list[str], fks: list[str]) -> str: | |
| """Format a single table as DDL-like string.""" | |
| cols_str = ", ".join(columns) | |
| result = f"TABLE {table_name} ({cols_str})" | |
| if fks: | |
| result += "\n" + "\n".join(f" FK: {fk}" for fk in fks) | |
| return result | |
| def schema_retrieval_node(state: AgentState, rag_retriever, db_pool) -> dict: | |
| """ | |
| Retrieve relevant database schema context for SQL generation. | |
| Uses hybrid search (vector + BM25) for best results. | |
| Reads retrieval_top_k from state (set by query_understanding agent). | |
| Falls back to 5 if the field is absent or invalid. | |
| """ | |
| user_query = state["user_query"] | |
| entities = state.get("entities", []) | |
| route_intent = state.get("route_intent", state.get("intent", "data_query")) | |
| trace_id = state.get("trace_id", "unknown") | |
| # ββ Resolve top_k from state with fallback βββββββββββ | |
| raw_top_k = state.get("retrieval_top_k", _DEFAULT_TOP_K) | |
| top_k = raw_top_k if raw_top_k in _VALID_TOP_K else _DEFAULT_TOP_K | |
| logger.info( | |
| "agent_started", | |
| agent="schema_retrieval", | |
| trace_id=trace_id, | |
| retrieval_top_k=top_k, | |
| top_k_source="state" if raw_top_k in _VALID_TOP_K else "default_fallback", | |
| complexity=state.get("complexity", "unknown"), | |
| ) | |
| try: | |
| # ββ Meta query: return full schema βββββββββββββββ | |
| if route_intent == "meta_query": | |
| full_schema = db_pool.get_full_schema() | |
| tables = db_pool.get_tables() | |
| logger.info( | |
| "schema_retrieved", | |
| trace_id=trace_id, | |
| source="meta_query", | |
| tables_found=len(tables), | |
| top_k_used=None, | |
| ) | |
| return { | |
| "relevant_schema": full_schema, | |
| "relevant_tables": tables, | |
| "retrieval_source": "meta_query", | |
| } | |
| # ββ Hybrid RAG retrieval βββββββββββββββββββββββββ | |
| # Combine user query with extracted entities for better retrieval precision. | |
| search_query = user_query | |
| if entities: | |
| search_query += " " + " ".join(entities) | |
| # Use expanded retrieval for multi-table queries (better recall) | |
| if len(entities) >= 2 and hasattr(rag_retriever, 'retrieve_expanded'): | |
| retrieved_docs = rag_retriever.retrieve_expanded( | |
| search_query, entities=entities, top_k=top_k | |
| ) | |
| else: | |
| retrieved_docs = rag_retriever.retrieve(search_query, top_k=top_k) | |
| if not retrieved_docs: | |
| # Fallback: return full schema when RAG returns nothing. | |
| logger.warning( | |
| "rag_empty_results", | |
| trace_id=trace_id, | |
| top_k_requested=top_k, | |
| fallback="full_schema", | |
| ) | |
| full_schema = db_pool.get_full_schema() | |
| return { | |
| "relevant_schema": full_schema, | |
| "relevant_tables": db_pool.get_tables(), | |
| "retrieval_source": "full_schema_fallback", | |
| } | |
| # Extract table names from retrieved documents. | |
| relevant_tables = [] | |
| for doc in retrieved_docs: | |
| for line in doc.split("\n"): | |
| if line.startswith("Table: "): | |
| table_name = line.replace("Table: ", "").strip() | |
| if table_name not in relevant_tables: | |
| relevant_tables.append(table_name) | |
| # Compress schema to DDL format for smaller prompt context | |
| raw_schema = "\n\n".join(retrieved_docs) | |
| relevant_schema = _compress_schema_to_ddl(raw_schema) | |
| logger.info( | |
| "schema_retrieved", | |
| trace_id=trace_id, | |
| source=f"rag_top_k:{top_k}", | |
| top_k_requested=top_k, | |
| docs_returned=len(retrieved_docs), | |
| tables_found=len(relevant_tables), | |
| tables=relevant_tables, | |
| compression_ratio=round(len(relevant_schema) / max(len(raw_schema), 1), 2), | |
| ) | |
| return { | |
| "relevant_schema": relevant_schema, | |
| "relevant_tables": relevant_tables, | |
| "retrieval_source": f"rag_top_k:{top_k}", | |
| } | |
| except Exception as e: | |
| logger.error("schema_retrieval_failed", trace_id=trace_id, error=str(e)) | |
| # Graceful fallback β never crash the pipeline over a retrieval failure. | |
| try: | |
| full_schema = db_pool.get_full_schema() | |
| return { | |
| "relevant_schema": full_schema, | |
| "relevant_tables": db_pool.get_tables(), | |
| "retrieval_source": "full_schema_fallback", | |
| } | |
| except Exception: | |
| return { | |
| "relevant_schema": "Schema unavailable", | |
| "relevant_tables": [], | |
| "retrieval_source": "error", | |
| "error": f"Schema retrieval failed: {str(e)}", | |
| "error_agent": "schema_retrieval", | |
| } | |