contsql 0.2.0__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {contsql-0.2.0 → contsql-0.2.3}/PKG-INFO +1 -1
- {contsql-0.2.0 → contsql-0.2.3}/contsql.egg-info/PKG-INFO +1 -1
- {contsql-0.2.0 → contsql-0.2.3}/contsql.py +115 -12
- {contsql-0.2.0 → contsql-0.2.3}/pyproject.toml +1 -1
- {contsql-0.2.0 → contsql-0.2.3}/README.md +0 -0
- {contsql-0.2.0 → contsql-0.2.3}/contsql.egg-info/SOURCES.txt +0 -0
- {contsql-0.2.0 → contsql-0.2.3}/contsql.egg-info/dependency_links.txt +0 -0
- {contsql-0.2.0 → contsql-0.2.3}/contsql.egg-info/entry_points.txt +0 -0
- {contsql-0.2.0 → contsql-0.2.3}/contsql.egg-info/requires.txt +0 -0
- {contsql-0.2.0 → contsql-0.2.3}/contsql.egg-info/top_level.txt +0 -0
- {contsql-0.2.0 → contsql-0.2.3}/setup.cfg +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# v0.2 | 2026-04-13 |
|
|
2
|
+
# v0.2.3 | 2026-04-13 | entity_id+unvan zorunlu kural + slash commands (/s /schema /trace /help)
|
|
3
3
|
"""contsql — Minimal DuckDB SQL agent. Soru sor, SQL üret, çalıştır, göster."""
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
@@ -58,9 +58,25 @@ def read_domain_notes(db_path):
|
|
|
58
58
|
return ""
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
# ── Referans algılama ──
|
|
62
|
+
|
|
63
|
+
REFERANS_TRIGGERS = [
|
|
64
|
+
"bu firma", "bunlar", "bunların", "yukarıdaki", "yukarıdakiler",
|
|
65
|
+
"aynı firma", "o firma", "önceki", "listedeki", "sonuçtaki",
|
|
66
|
+
"bu müşteri", "bu muta", "onların",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def has_reference_trigger(question):
|
|
71
|
+
"""Kullanıcı sorusu önceki sorgu sonucuna referans veriyor mu?"""
|
|
72
|
+
q_lower = question.lower()
|
|
73
|
+
return any(trigger in q_lower for trigger in REFERANS_TRIGGERS)
|
|
74
|
+
|
|
75
|
+
|
|
61
76
|
# ── System prompt ──
|
|
62
77
|
|
|
63
|
-
def build_system_prompt(schema_text, domain_text="", last_result_entities=None
|
|
78
|
+
def build_system_prompt(schema_text, domain_text="", last_result_entities=None,
|
|
79
|
+
question=None):
|
|
64
80
|
prompt = f"""Sen bir SQL asistanısın. Kullanıcının sorusuna uygun SQL yaz.
|
|
65
81
|
|
|
66
82
|
Kurallar:
|
|
@@ -69,6 +85,7 @@ Kurallar:
|
|
|
69
85
|
- SQL öncesi veya sonrası açıklama ekleme.
|
|
70
86
|
- Emin değilsen "Bu soruyu mevcut tablolarla cevaplayamıyorum" de.
|
|
71
87
|
- Veri uydurma. Sorgu sonucu olmadan liste verme.
|
|
88
|
+
- HER sorguda entity_id ve unvan kolonlarını dahil et. Firmalar bu iki alanla tanımlanır. Sadece COUNT/SUM gibi tek değer döndüren aggregation sorgularında entity_id gerekmez.
|
|
72
89
|
- String karşılaştırmalarında LIKE yerine her zaman ILIKE kullan. Türkçe karakter eşleştirmesi (İ↔i, I↔ı, Ş↔ş, Ü↔ü, Ö↔ö, Ç↔ç, Ğ↔ğ) için ILIKE şart.
|
|
73
90
|
|
|
74
91
|
Veritabanı şeması:
|
|
@@ -76,11 +93,10 @@ Veritabanı şeması:
|
|
|
76
93
|
"""
|
|
77
94
|
if domain_text:
|
|
78
95
|
prompt += f"\nDomain bilgisi:\n{domain_text}\n"
|
|
79
|
-
if last_result_entities:
|
|
96
|
+
if last_result_entities and question and has_reference_trigger(question):
|
|
80
97
|
prompt += (
|
|
81
98
|
f"\nÖNCEKİ SORGU SONUCUNDAKI FİRMALAR (entity_id): {last_result_entities}\n"
|
|
82
|
-
"
|
|
83
|
-
"referans verirse bu entity_id listesini WHERE koşulunda kullan.\n"
|
|
99
|
+
"Bu entity_id listesini WHERE koşulunda kullan.\n"
|
|
84
100
|
)
|
|
85
101
|
return prompt
|
|
86
102
|
|
|
@@ -154,6 +170,33 @@ def ask_model(system_prompt, question):
|
|
|
154
170
|
return f"LLM HATA: {e}", time.time() - t0, 0
|
|
155
171
|
|
|
156
172
|
|
|
173
|
+
def generate_sql(conn, question, last_result_entities=None, domain_text=""):
|
|
174
|
+
"""Soru → SQL string. Test runner için callable. Başarısızsa None."""
|
|
175
|
+
schema_text = read_schema(conn)
|
|
176
|
+
system_prompt = build_system_prompt(schema_text, domain_text, last_result_entities,
|
|
177
|
+
question=question)
|
|
178
|
+
response, _, _ = ask_model(system_prompt, question)
|
|
179
|
+
sql = extract_sql(response)
|
|
180
|
+
if not sql or check_sql_safety(sql):
|
|
181
|
+
return None
|
|
182
|
+
sql = _like_to_ilike(sql)
|
|
183
|
+
|
|
184
|
+
# Ambiguous column retry: EXPLAIN ile ön kontrol
|
|
185
|
+
try:
|
|
186
|
+
conn.execute(f"EXPLAIN {sql}")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
if "ambiguous" not in str(e).lower():
|
|
189
|
+
return sql
|
|
190
|
+
retry_q = f"{question}\n\nÖNCEKİ SQL HATA: {e}\nJOIN'de tablo alias kullan."
|
|
191
|
+
resp2, _, _ = ask_model(system_prompt, retry_q)
|
|
192
|
+
sql2 = extract_sql(resp2)
|
|
193
|
+
if sql2 and not check_sql_safety(sql2):
|
|
194
|
+
return _like_to_ilike(sql2)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
return sql
|
|
198
|
+
|
|
199
|
+
|
|
157
200
|
# ── Result formatting ──
|
|
158
201
|
|
|
159
202
|
def _fmt_value(v):
|
|
@@ -269,17 +312,72 @@ def run_query(conn, system_prompt, question):
|
|
|
269
312
|
print(" ⚠ Önceki sorgu çok geniş — firma referansı için soruyu daraltın.")
|
|
270
313
|
return entities
|
|
271
314
|
except duckdb.Error as e:
|
|
315
|
+
# Ambiguous column retry
|
|
316
|
+
if "ambiguous" in str(e).lower():
|
|
317
|
+
print(f"🔄 Ambiguous column, retry...")
|
|
318
|
+
retry_q = f"{question}\n\nSQL HATA: {e}\nJOIN'de tablo alias kullan."
|
|
319
|
+
resp2, _, _ = ask_model(system_prompt, retry_q)
|
|
320
|
+
sql2 = extract_sql(resp2)
|
|
321
|
+
if sql2 and not check_sql_safety(sql2):
|
|
322
|
+
sql2 = _like_to_ilike(sql2)
|
|
323
|
+
print(f"🔍 Retry SQL: {sql2}")
|
|
324
|
+
try:
|
|
325
|
+
result = conn.execute(sql2)
|
|
326
|
+
columns = [desc[0] for desc in result.description]
|
|
327
|
+
rows = result.fetchall()
|
|
328
|
+
query_ms = (time.time() - t0) * 1000
|
|
329
|
+
print(f"\n📊 SONUÇ ({len(rows)} satır, {query_ms:.0f}ms)")
|
|
330
|
+
print(format_table(columns, rows))
|
|
331
|
+
entities = _extract_entity_ids(columns, rows)
|
|
332
|
+
return entities
|
|
333
|
+
except duckdb.Error as e2:
|
|
334
|
+
print(f"\n❌ Retry hatası: {e2}")
|
|
335
|
+
return None
|
|
272
336
|
print(f"\n❌ SQL hatası: {e}")
|
|
273
337
|
print(f"🔍 SQL: {sql}")
|
|
274
338
|
return None
|
|
275
339
|
|
|
276
340
|
|
|
341
|
+
def handle_slash_command(cmd, state):
|
|
342
|
+
"""Slash command işle. True → normal sorgu akışına girme."""
|
|
343
|
+
cmd = cmd.strip().lower()
|
|
344
|
+
|
|
345
|
+
if cmd == "/s":
|
|
346
|
+
state["last_result_entities"] = None
|
|
347
|
+
print("🧹 Bellek temizlendi.")
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
if cmd == "/schema":
|
|
351
|
+
print(f"\n{state['schema_text']}\n")
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
if cmd == "/trace":
|
|
355
|
+
state["trace"] = not state.get("trace", False)
|
|
356
|
+
print(f"🔍 Trace: {'açık' if state['trace'] else 'kapalı'}")
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
if cmd == "/help":
|
|
360
|
+
print("Komutlar:")
|
|
361
|
+
print(" /s — önceki sorgu hafızasını temizle")
|
|
362
|
+
print(" /schema — veritabanı şemasını göster")
|
|
363
|
+
print(" /trace — SQL trace modunu aç/kapa")
|
|
364
|
+
print(" /help — bu mesaj")
|
|
365
|
+
print(" quit — çıkış")
|
|
366
|
+
return True
|
|
367
|
+
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
|
|
277
371
|
def interactive_loop(conn, schema_text, domain_text):
|
|
278
372
|
"""REPL döngüsü."""
|
|
279
|
-
print(f"\ncontsql hazır. Model: {MODEL}")
|
|
373
|
+
print(f"\ncontsql hazır. Model: {MODEL} | /help komutlar")
|
|
280
374
|
print("Çıkmak için: quit/exit/q\n")
|
|
281
375
|
|
|
282
|
-
|
|
376
|
+
state = {
|
|
377
|
+
"last_result_entities": None,
|
|
378
|
+
"trace": False,
|
|
379
|
+
"schema_text": schema_text,
|
|
380
|
+
}
|
|
283
381
|
|
|
284
382
|
while True:
|
|
285
383
|
try:
|
|
@@ -292,14 +390,18 @@ def interactive_loop(conn, schema_text, domain_text):
|
|
|
292
390
|
continue
|
|
293
391
|
if question.lower() in ("quit", "exit", "q", "çık"):
|
|
294
392
|
break
|
|
295
|
-
|
|
296
|
-
|
|
393
|
+
|
|
394
|
+
if question.startswith("/"):
|
|
395
|
+
if not handle_slash_command(question, state):
|
|
396
|
+
print(f"Bilinmeyen komut: {question}. /help yazın.")
|
|
297
397
|
continue
|
|
298
398
|
|
|
299
|
-
system_prompt = build_system_prompt(schema_text, domain_text,
|
|
399
|
+
system_prompt = build_system_prompt(schema_text, domain_text,
|
|
400
|
+
state["last_result_entities"],
|
|
401
|
+
question=question)
|
|
300
402
|
entities = run_query(conn, system_prompt, question)
|
|
301
403
|
if entities is not None:
|
|
302
|
-
last_result_entities = entities
|
|
404
|
+
state["last_result_entities"] = entities
|
|
303
405
|
print()
|
|
304
406
|
|
|
305
407
|
|
|
@@ -331,7 +433,8 @@ def main():
|
|
|
331
433
|
|
|
332
434
|
# Tek soru veya interaktif
|
|
333
435
|
if args.question:
|
|
334
|
-
system_prompt = build_system_prompt(schema_text, domain_text
|
|
436
|
+
system_prompt = build_system_prompt(schema_text, domain_text,
|
|
437
|
+
question=args.question)
|
|
335
438
|
run_query(conn, system_prompt, args.question)
|
|
336
439
|
else:
|
|
337
440
|
interactive_loop(conn, schema_text, domain_text)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|