Lukynnnn commited on
Commit
f70456a
·
verified ·
1 Parent(s): 16cf24c

Upload mcp_database_universal/formatters/llm.py with huggingface_hub

Browse files
mcp_database_universal/formatters/llm.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LLM-friendly output formatting for database results."""
2
+
3
+ from dataclasses import dataclass
4
+ from mcp_database_universal.config import DatabaseConfig
5
+ from mcp_database_universal.engines.base import DBInfo, TableInfo, TableDetail, TableStats, QueryResult
6
+
7
+
8
+ TYPE_TRANSLATIONS = {
9
+ "INTEGER": "integer",
10
+ "INT": "integer",
11
+ "BIGINT": "large integer",
12
+ "SMALLINT": "small integer",
13
+ "TINYINT": "tiny integer",
14
+ "REAL": "decimal number",
15
+ "FLOAT": "decimal number",
16
+ "DOUBLE": "decimal number",
17
+ "DOUBLE PRECISION": "decimal number",
18
+ "NUMERIC": "precise decimal",
19
+ "DECIMAL": "precise decimal",
20
+ "TEXT": "text",
21
+ "VARCHAR": "text",
22
+ "CHAR": "text",
23
+ "BOOLEAN": "true/false",
24
+ "BOOL": "true/false",
25
+ "DATETIME": "date and time",
26
+ "DATE": "date only",
27
+ "TIMESTAMP": "date and time",
28
+ "TIMESTAMP WITH TIME ZONE": "date and time (timezone)",
29
+ "TIMESTAMP WITHOUT TIME ZONE": "date and time",
30
+ "BLOB": "binary data",
31
+ "BYTEA": "binary data",
32
+ "JSON": "JSON data",
33
+ "JSONB": "JSON data (optimized)",
34
+ "UUID": "unique identifier",
35
+ "ARRAY": "list of values",
36
+ "SERIAL": "auto-increment integer",
37
+ "BIGSERIAL": "auto-increment large integer",
38
+ }
39
+
40
+
41
+ class LLMFormatter:
42
+ def __init__(self, config: DatabaseConfig):
43
+ self.config = config
44
+
45
+ def translate_type(self, raw_type: str) -> str:
46
+ upper = raw_type.upper().strip()
47
+ if "(" in upper:
48
+ base = upper.split("(")[0].strip()
49
+ size = upper.split("(")[1].rstrip(")")
50
+ translated = TYPE_TRANSLATIONS.get(base, base.lower())
51
+ return f"{translated} (max {size} chars)" if "text" in translated else translated
52
+ return TYPE_TRANSLATIONS.get(upper, raw_type.lower())
53
+
54
+ def format_number(self, n) -> str:
55
+ if n is None:
56
+ return "(empty)"
57
+ if isinstance(n, float):
58
+ return f"{n:,.2f}"
59
+ if isinstance(n, int):
60
+ return f"{n:,}"
61
+ return str(n)
62
+
63
+ def format_value(self, v) -> str:
64
+ if v is None:
65
+ return "(empty)"
66
+ if isinstance(v, float):
67
+ return f"{v:,.2f}"
68
+ if isinstance(v, int):
69
+ return f"{v:,}"
70
+ return str(v)
71
+
72
+ def truncate_for_llm(self, text: str) -> tuple[str, bool]:
73
+ max_bytes = self.config.max_output_bytes
74
+ if len(text.encode("utf-8")) <= max_bytes:
75
+ return text, False
76
+ lines = text.split("\n")
77
+ truncated_lines = []
78
+ byte_count = 0
79
+ for line in lines:
80
+ line_bytes = len(line.encode("utf-8")) + 1
81
+ if byte_count + line_bytes > max_bytes - 200:
82
+ truncated_lines.append("... (output truncated)")
83
+ break
84
+ truncated_lines.append(line)
85
+ byte_count += line_bytes
86
+ return "\n".join(truncated_lines), True
87
+
88
+ def format_db_info(self, info: DBInfo) -> str:
89
+ lines = [
90
+ f"## Database Connection OK",
91
+ f"- **Engine:** {info.engine}",
92
+ f"- **Version:** {info.version}",
93
+ f"- **Name:** {info.name}",
94
+ f"- **Size:** {info.size_approx}",
95
+ ]
96
+ text = "\n".join(lines)
97
+ text, _ = self.truncate_for_llm(text)
98
+ return text
99
+
100
+ def format_table_list(self, tables: list[TableInfo]) -> str:
101
+ if not tables:
102
+ return "## Tables\nNo tables found in this database."
103
+
104
+ lines = [
105
+ f"## Tables ({len(tables)} total)",
106
+ "",
107
+ "| Table | Rows | Columns | FK In | FK Out |",
108
+ "|-------|------|---------|-------|--------|",
109
+ ]
110
+
111
+ for t in tables:
112
+ lines.append(
113
+ f"| {t.name} | {self.format_number(t.row_count)} | "
114
+ f"{t.column_count} | {t.foreign_keys_in} | {t.foreign_keys_out} |"
115
+ )
116
+
117
+ text = "\n".join(lines)
118
+ text, _ = self.truncate_for_llm(text)
119
+ return text
120
+
121
+ def format_table_detail(self, detail: TableDetail) -> str:
122
+ lines = [
123
+ f"## Table: {detail.name}",
124
+ "",
125
+ "### Columns",
126
+ "| # | Name | Type | Nullable | Key |",
127
+ "|---|------|------|----------|-----|",
128
+ ]
129
+
130
+ for i, col in enumerate(detail.columns, 1):
131
+ key = ""
132
+ if col.is_primary_key:
133
+ key = "PK"
134
+ elif col.is_foreign_key:
135
+ key = f"FK -> {col.foreign_key_table}.{col.foreign_key_column}"
136
+ lines.append(
137
+ f"| {i} | {col.name} | {self.translate_type(col.type)} | "
138
+ f"{'yes' if col.nullable else 'no'} | {key} |"
139
+ )
140
+
141
+ if detail.foreign_keys:
142
+ lines.extend(["", "### Relationships"])
143
+ for fk in detail.foreign_keys:
144
+ lines.append(
145
+ f"- `{detail.name}.{fk.column}` -> "
146
+ f"`{fk.references_table}.{fk.references_column}`"
147
+ )
148
+
149
+ if detail.indexes:
150
+ lines.extend(["", "### Indexes"])
151
+ for idx in detail.indexes:
152
+ unique = " UNIQUE" if idx.unique else ""
153
+ lines.append(f"- `{idx.name}` ON ({', '.join(idx.columns)}){unique}")
154
+
155
+ if detail.sample_data:
156
+ lines.extend(["", "### Sample Data (first 5 rows)"])
157
+ if detail.sample_data:
158
+ headers = list(detail.sample_data[0].keys())
159
+ lines.append("| " + " | ".join(headers) + " |")
160
+ lines.append("| " + " | ".join(["---"] * len(headers)) + " |")
161
+ for row in detail.sample_data:
162
+ vals = [self.format_value(row.get(h)) for h in headers]
163
+ lines.append("| " + " | ".join(vals) + " |")
164
+
165
+ if detail.stats:
166
+ lines.extend(["", "### Statistics"])
167
+ lines.append(f"- **Row count:** {self.format_number(detail.stats.row_count)}")
168
+ lines.append(f"- **Avg row size:** {detail.stats.avg_row_size}")
169
+ lines.append(f"- **Total size:** {detail.stats.total_size}")
170
+
171
+ if detail.stats.null_counts:
172
+ non_zero = {k: v for k, v in detail.stats.null_counts.items() if v > 0}
173
+ if non_zero:
174
+ lines.append("- **NULL counts:**")
175
+ for col, count in non_zero.items():
176
+ lines.append(f" - {col}: {self.format_number(count)}")
177
+
178
+ text = "\n".join(lines)
179
+ text, _ = self.truncate_for_llm(text)
180
+ return text
181
+
182
+ def format_query_result(self, result: QueryResult) -> str:
183
+ lines = [
184
+ "## Query Results",
185
+ f"**SQL:** `{result.sql}`",
186
+ f"**Rows:** {result.row_count}{' (truncated)' if result.truncated else ''}",
187
+ f"**Time:** {result.execution_time_ms}ms",
188
+ ]
189
+
190
+ if result.warning:
191
+ lines.append(f"**Warning:** {result.warning}")
192
+
193
+ if result.columns and result.rows:
194
+ lines.append("")
195
+ lines.append("| " + " | ".join(result.columns) + " |")
196
+ lines.append("| " + " | ".join(["---"] * len(result.columns)) + " |")
197
+ for row in result.rows:
198
+ vals = [self.format_value(row.get(c)) for c in result.columns]
199
+ lines.append("| " + " | ".join(vals) + " |")
200
+ elif not result.rows:
201
+ lines.append("\n*No rows returned.*")
202
+
203
+ text = "\n".join(lines)
204
+ text, _ = self.truncate_for_llm(text)
205
+ return text
206
+
207
+ def format_profile(self, profile_data: dict) -> str:
208
+ lines = ["## Database Profile", ""]
209
+
210
+ if "db_info" in profile_data:
211
+ info = profile_data["db_info"]
212
+ lines.extend([
213
+ f"**Engine:** {info.engine} {info.version}",
214
+ f"**Size:** {info.size_approx}",
215
+ f"**Tables:** {profile_data.get('table_count', 'unknown')}",
216
+ ])
217
+
218
+ if "tables" in profile_data:
219
+ lines.extend(["", "### Overview", "| Table | Rows | Columns | FK In | FK Out |", "|-------|------|---------|-------|--------|"])
220
+ for t in profile_data["tables"]:
221
+ lines.append(
222
+ f"| {t.name} | {self.format_number(t.row_count)} | "
223
+ f"{t.column_count} | {t.foreign_keys_in} | {t.foreign_keys_out} |"
224
+ )
225
+
226
+ if "relationships" in profile_data and profile_data["relationships"]:
227
+ lines.extend(["", f"### Relationships ({len(profile_data['relationships'])})"])
228
+ for rel in profile_data["relationships"]:
229
+ explicit = "explicit" if rel.is_explicit else "inferred"
230
+ lines.append(f"- {rel.from_table}.{rel.from_column} -> {rel.to_table}.{rel.to_column} ({rel.cardinality}, {explicit})")
231
+
232
+ if "junction_tables" in profile_data and profile_data["junction_tables"]:
233
+ lines.extend(["", "### Junction Tables (many-to-many)"])
234
+ for jt in profile_data["junction_tables"]:
235
+ lines.append(f"- `{jt}`")
236
+
237
+ if "table_stats" in profile_data:
238
+ lines.extend(["", "### Table Statistics"])
239
+ for table_name, stats in profile_data["table_stats"].items():
240
+ lines.append(f"\n#### {table_name}")
241
+ lines.append(f"- Rows: {self.format_number(stats.row_count)}")
242
+ lines.append(f"- Size: {stats.total_size}")
243
+ if stats.null_counts:
244
+ non_zero = {k: v for k, v in stats.null_counts.items() if v > 0}
245
+ if non_zero:
246
+ lines.append("- NULL columns:")
247
+ for col, count in non_zero.items():
248
+ lines.append(f" - {col}: {self.format_number(count)}")
249
+
250
+ text = "\n".join(lines)
251
+ text, _ = self.truncate_for_llm(text)
252
+ return text
253
+
254
+ def format_schema_graph(self, mermaid: str) -> str:
255
+ lines = [
256
+ "## Schema Graph",
257
+ "",
258
+ "```mermaid",
259
+ mermaid,
260
+ "```",
261
+ ]
262
+ return "\n".join(lines)