contsql 0.2.2__tar.gz → 0.2.9__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.2 → contsql-0.2.9}/PKG-INFO +1 -1
- {contsql-0.2.2 → contsql-0.2.9}/contsql.egg-info/PKG-INFO +1 -1
- {contsql-0.2.2 → contsql-0.2.9}/contsql.py +276 -55
- {contsql-0.2.2 → contsql-0.2.9}/pyproject.toml +1 -1
- {contsql-0.2.2 → contsql-0.2.9}/README.md +0 -0
- {contsql-0.2.2 → contsql-0.2.9}/contsql.egg-info/SOURCES.txt +0 -0
- {contsql-0.2.2 → contsql-0.2.9}/contsql.egg-info/dependency_links.txt +0 -0
- {contsql-0.2.2 → contsql-0.2.9}/contsql.egg-info/entry_points.txt +0 -0
- {contsql-0.2.2 → contsql-0.2.9}/contsql.egg-info/requires.txt +0 -0
- {contsql-0.2.2 → contsql-0.2.9}/contsql.egg-info/top_level.txt +0 -0
- {contsql-0.2.2 → contsql-0.2.9}/setup.cfg +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# v0.2.
|
|
2
|
+
# v0.2.9 | 2026-04-13 | JSONL session loglama — her şey kaydedilir, default açık
|
|
3
3
|
"""contsql — Minimal DuckDB SQL agent. Soru sor, SQL üret, çalıştır, göster."""
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
@@ -22,6 +22,27 @@ TIMEOUT = int(os.environ.get("CONTSQL_TIMEOUT", "120"))
|
|
|
22
22
|
|
|
23
23
|
BANNED_SQL = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE", "TRUNCATE", "EXEC"]
|
|
24
24
|
|
|
25
|
+
# ── Loglama ──
|
|
26
|
+
|
|
27
|
+
_LOG_FILE = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _init_log(db_path):
|
|
31
|
+
"""Log dosyasını aç. DB'nin yanına contsql_log.jsonl yazar."""
|
|
32
|
+
global _LOG_FILE
|
|
33
|
+
log_path = Path(db_path).parent / "contsql_log.jsonl"
|
|
34
|
+
_LOG_FILE = open(log_path, "a", encoding="utf-8")
|
|
35
|
+
return log_path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _log(event, **data):
|
|
39
|
+
"""Tek satır JSONL log yaz."""
|
|
40
|
+
if _LOG_FILE is None:
|
|
41
|
+
return
|
|
42
|
+
entry = {"ts": time.strftime("%Y-%m-%d %H:%M:%S"), "event": event, **data}
|
|
43
|
+
_LOG_FILE.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
44
|
+
_LOG_FILE.flush()
|
|
45
|
+
|
|
25
46
|
|
|
26
47
|
# ── Schema discovery ──
|
|
27
48
|
|
|
@@ -45,6 +66,31 @@ def read_schema(conn):
|
|
|
45
66
|
return "\n".join(lines)
|
|
46
67
|
|
|
47
68
|
|
|
69
|
+
def build_column_owner_map(conn):
|
|
70
|
+
"""Her kolonun hangi tablo(lar)da olduğunu çıkar. Açılışta bir kez çalışır."""
|
|
71
|
+
tables = conn.execute(
|
|
72
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
|
|
73
|
+
).fetchall()
|
|
74
|
+
col_map = {}
|
|
75
|
+
for (tname,) in tables:
|
|
76
|
+
cols = conn.execute(f"""
|
|
77
|
+
SELECT column_name FROM information_schema.columns
|
|
78
|
+
WHERE table_schema='main' AND table_name='{tname}'
|
|
79
|
+
""").fetchall()
|
|
80
|
+
for (cname,) in cols:
|
|
81
|
+
col_map.setdefault(cname, []).append(tname)
|
|
82
|
+
return col_map
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def format_column_hints(col_map):
|
|
86
|
+
"""Tek tabloya ait kolonları vurgula — bunlar JOIN gerektirir."""
|
|
87
|
+
lines = ["KOLON SAHİPLİĞİ (JOIN gerektiren kolonlar):"]
|
|
88
|
+
for col, tables in sorted(col_map.items()):
|
|
89
|
+
if len(tables) == 1:
|
|
90
|
+
lines.append(f" {col} → SADECE {tables[0]}")
|
|
91
|
+
return "\n".join(lines)
|
|
92
|
+
|
|
93
|
+
|
|
48
94
|
def read_domain_notes(db_path):
|
|
49
95
|
"""domain_notes.txt veya ews_domain.yaml varsa oku."""
|
|
50
96
|
for name in ("domain_notes.txt", "ews_domain.yaml"):
|
|
@@ -73,10 +119,29 @@ def has_reference_trigger(question):
|
|
|
73
119
|
return any(trigger in q_lower for trigger in REFERANS_TRIGGERS)
|
|
74
120
|
|
|
75
121
|
|
|
122
|
+
SORGU_TRIGGERS = [
|
|
123
|
+
"yanına ekle", "yanına da ekle", "yanına", "buna ekle", "buna da ekle",
|
|
124
|
+
"kolonu da ekle", "kolonunu da ekle", "bir de", "aynısına",
|
|
125
|
+
"aynı sorguya", "aynı sorgu", "üstüne ekle", "ekle yanına",
|
|
126
|
+
"göster yanında", "da göster", "da getir", "de göster", "de getir",
|
|
127
|
+
"tablodan", "tablodaki", "tabloyu", "tabloya",
|
|
128
|
+
"çıkart", "çıkar", "kaldır", "at şunu", "filtrele", "daralt",
|
|
129
|
+
"sadece", "hariç", "hariç tut",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
MAX_SQL_CONTEXT_LENGTH = 500
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def has_query_trigger(question):
|
|
136
|
+
"""Kullanıcı önceki sorguyu modifiye etmek mi istiyor?"""
|
|
137
|
+
q_lower = question.lower()
|
|
138
|
+
return any(trigger in q_lower for trigger in SORGU_TRIGGERS)
|
|
139
|
+
|
|
140
|
+
|
|
76
141
|
# ── System prompt ──
|
|
77
142
|
|
|
78
143
|
def build_system_prompt(schema_text, domain_text="", last_result_entities=None,
|
|
79
|
-
question=None):
|
|
144
|
+
question=None, last_sql=None, column_hints=""):
|
|
80
145
|
prompt = f"""Sen bir SQL asistanısın. Kullanıcının sorusuna uygun SQL yaz.
|
|
81
146
|
|
|
82
147
|
Kurallar:
|
|
@@ -85,7 +150,7 @@ Kurallar:
|
|
|
85
150
|
- SQL öncesi veya sonrası açıklama ekleme.
|
|
86
151
|
- Emin değilsen "Bu soruyu mevcut tablolarla cevaplayamıyorum" de.
|
|
87
152
|
- 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
|
|
153
|
+
- 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.
|
|
89
154
|
- 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.
|
|
90
155
|
|
|
91
156
|
Veritabanı şeması:
|
|
@@ -93,11 +158,39 @@ Veritabanı şeması:
|
|
|
93
158
|
"""
|
|
94
159
|
if domain_text:
|
|
95
160
|
prompt += f"\nDomain bilgisi:\n{domain_text}\n"
|
|
161
|
+
if column_hints:
|
|
162
|
+
prompt += f"\n{column_hints}\n"
|
|
163
|
+
if last_sql and question and has_query_trigger(question):
|
|
164
|
+
if len(last_sql) <= MAX_SQL_CONTEXT_LENGTH:
|
|
165
|
+
prompt += f"""\nÖNCEKİ SQL'İ MODİFİYE ET:
|
|
166
|
+
|
|
167
|
+
{last_sql}
|
|
168
|
+
|
|
169
|
+
KURALLAR:
|
|
170
|
+
1. Yukarıdaki SQL'i TEMEL AL. Sıfırdan sorgu YAZMA.
|
|
171
|
+
2. WHERE koşullarını AYNEN KORU (filtre, LIMIT, entity_id değerleri dahil).
|
|
172
|
+
3. JOIN'leri AYNEN KORU. Yeni kolon için ek JOIN gerekiyorsa EKLE ama mevcutları ÇIKARMA.
|
|
173
|
+
4. Sadece istenen değişikliği yap:
|
|
174
|
+
- "ekle" / "göster" → SELECT'e kolon ekle
|
|
175
|
+
- "çıkar" / "kaldır" → SELECT'ten kolon çıkar
|
|
176
|
+
- "filtrele" / "sadece" → WHERE'e koşul ekle
|
|
177
|
+
5. LIMIT varsa KORU. ORDER BY varsa KORU.
|
|
178
|
+
6. Kolon eklerken dönem belirsizse son dönemi kullan: f.donem = (SELECT MAX(donem) FROM fact_periodic). Tüm dönemleri getirme.
|
|
179
|
+
"""
|
|
96
180
|
if last_result_entities and question and has_reference_trigger(question):
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
181
|
+
ids = last_result_entities
|
|
182
|
+
ids_quoted = ", ".join(str(eid) for eid in ids)
|
|
183
|
+
prompt += f"""\nÖNCEKİ SORGU FİRMALARI (entity_id): {ids}
|
|
184
|
+
|
|
185
|
+
Bu entity_id değerlerini SQL'de DOĞRUDAN yaz.
|
|
186
|
+
DOĞRU: WHERE fp.entity_id IN ({ids_quoted})
|
|
187
|
+
YANLIŞ: WHERE entity_id = :muta
|
|
188
|
+
YANLIŞ: WHERE entity_id = 'MUTA_DEGERI'
|
|
189
|
+
YANLIŞ: WHERE entity_id = '1234567890'
|
|
190
|
+
|
|
191
|
+
Placeholder, parametre, bind variable, örnek değer KULLANMA.
|
|
192
|
+
Gerçek entity_id değerlerini doğrudan SQL string'ine yaz.
|
|
193
|
+
"""
|
|
101
194
|
return prompt
|
|
102
195
|
|
|
103
196
|
|
|
@@ -117,6 +210,9 @@ def extract_sql(response_text):
|
|
|
117
210
|
|
|
118
211
|
def _like_to_ilike(sql):
|
|
119
212
|
"""LIKE → ILIKE guardrail. String literal içindekilere dokunmaz."""
|
|
213
|
+
# ILIKE(col, pattern) fonksiyon syntax'ini col ILIKE pattern'e çevir
|
|
214
|
+
sql = re.sub(r'\bILIKE\s*\(\s*(\w+\.?\w*)\s*,\s*', r'\1 ILIKE ', sql,
|
|
215
|
+
flags=re.IGNORECASE)
|
|
120
216
|
return re.sub(
|
|
121
217
|
r"""(?x)
|
|
122
218
|
( # Grup 1: string literal — atla
|
|
@@ -170,24 +266,77 @@ def ask_model(system_prompt, question):
|
|
|
170
266
|
return f"LLM HATA: {e}", time.time() - t0, 0
|
|
171
267
|
|
|
172
268
|
|
|
173
|
-
def
|
|
269
|
+
def _short_error(msg):
|
|
270
|
+
"""Trace için kısa hata özeti."""
|
|
271
|
+
first = str(msg).split('\n')[0]
|
|
272
|
+
return first[:77] + "..." if len(first) > 80 else first
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _extract_column_hint(error_message, col_map):
|
|
276
|
+
"""Hata mesajından kolon adını çıkar, doğru tabloyu bul."""
|
|
277
|
+
err = str(error_message)
|
|
278
|
+
m = re.search(r'[Cc]olumn "(\w+)" not found', err)
|
|
279
|
+
if not m:
|
|
280
|
+
m = re.search(r'does not have a column named "(\w+)"', err)
|
|
281
|
+
if not m:
|
|
282
|
+
return None
|
|
283
|
+
col_name = m.group(1)
|
|
284
|
+
if col_name not in col_map:
|
|
285
|
+
return f"'{col_name}' kolonu veritabanında hiç yok. Schema'yı kontrol et."
|
|
286
|
+
owners = col_map[col_name]
|
|
287
|
+
if len(owners) == 1:
|
|
288
|
+
return (f"'{col_name}' kolonu SADECE '{owners[0]}' tablosundadır. "
|
|
289
|
+
f"JOIN {owners[0]} ... yapıp {owners[0]}.{col_name} olarak eriş.")
|
|
290
|
+
tables_str = ", ".join(owners)
|
|
291
|
+
return (f"'{col_name}' kolonu şu tablolarda var: {tables_str}. "
|
|
292
|
+
f"Doğru tabloyu JOIN ile ekle ve tablo alias'ı kullan.")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _build_retry_prompt(question, failed_sql, error_message, col_map=None):
|
|
296
|
+
"""Hata sonrası modele gönderilecek retry prompt."""
|
|
297
|
+
hint_block = ""
|
|
298
|
+
if col_map:
|
|
299
|
+
hint = _extract_column_hint(error_message, col_map)
|
|
300
|
+
if hint:
|
|
301
|
+
hint_block = f"\n\nİPUCU: {hint}\n"
|
|
302
|
+
|
|
303
|
+
return f"""Kullanıcı sorusu: {question}
|
|
304
|
+
|
|
305
|
+
Ürettiğin SQL hata verdi:
|
|
306
|
+
|
|
307
|
+
SQL:
|
|
308
|
+
{failed_sql}
|
|
309
|
+
|
|
310
|
+
Hata:
|
|
311
|
+
{error_message}
|
|
312
|
+
{hint_block}
|
|
313
|
+
Bu hatayı düzelt ve yeni SQL yaz. Kurallar:
|
|
314
|
+
- Hata mesajındaki ipucunu kullan
|
|
315
|
+
- Doğru tabloyu JOIN ile ekle
|
|
316
|
+
- Her kolon referansında tablo alias'ı kullan
|
|
317
|
+
- Placeholder, parametre, bind variable KULLANMA — gerçek değerleri yaz
|
|
318
|
+
- Sadece düzeltilmiş SQL yaz, açıklama yapma
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def generate_sql(conn, question, last_result_entities=None, domain_text="",
|
|
323
|
+
col_map=None):
|
|
174
324
|
"""Soru → SQL string. Test runner için callable. Başarısızsa None."""
|
|
175
325
|
schema_text = read_schema(conn)
|
|
326
|
+
column_hints = format_column_hints(col_map) if col_map else ""
|
|
176
327
|
system_prompt = build_system_prompt(schema_text, domain_text, last_result_entities,
|
|
177
|
-
question=question)
|
|
328
|
+
question=question, column_hints=column_hints)
|
|
178
329
|
response, _, _ = ask_model(system_prompt, question)
|
|
179
330
|
sql = extract_sql(response)
|
|
180
331
|
if not sql or check_sql_safety(sql):
|
|
181
332
|
return None
|
|
182
333
|
sql = _like_to_ilike(sql)
|
|
183
334
|
|
|
184
|
-
#
|
|
335
|
+
# Genel SQL hata retry: EXPLAIN ile ön kontrol
|
|
185
336
|
try:
|
|
186
337
|
conn.execute(f"EXPLAIN {sql}")
|
|
187
338
|
except Exception as e:
|
|
188
|
-
|
|
189
|
-
return sql
|
|
190
|
-
retry_q = f"{question}\n\nÖNCEKİ SQL HATA: {e}\nJOIN'de tablo alias kullan."
|
|
339
|
+
retry_q = _build_retry_prompt(question, sql, e, col_map)
|
|
191
340
|
resp2, _, _ = ask_model(system_prompt, retry_q)
|
|
192
341
|
sql2 = extract_sql(resp2)
|
|
193
342
|
if sql2 and not check_sql_safety(sql2):
|
|
@@ -265,10 +414,12 @@ def _extract_entity_ids(columns, rows, max_entities=100):
|
|
|
265
414
|
return ids or None
|
|
266
415
|
|
|
267
416
|
|
|
268
|
-
def run_query(conn, system_prompt, question):
|
|
269
|
-
"""Tek soru → SQL → çalıştır → sonuç. entity_id listesi döndürür."""
|
|
417
|
+
def run_query(conn, system_prompt, question, col_map=None):
|
|
418
|
+
"""Tek soru → SQL → çalıştır → sonuç. (entity_id listesi, sql) tuple döndürür."""
|
|
270
419
|
# 1. Model'e sor
|
|
271
420
|
response, elapsed, tokens = ask_model(system_prompt, question)
|
|
421
|
+
_log("model", question=question, elapsed=round(elapsed, 1), tokens=tokens,
|
|
422
|
+
response=response)
|
|
272
423
|
|
|
273
424
|
print(f"\n💭 MODEL ({elapsed:.1f}s, ~{tokens} tok)")
|
|
274
425
|
if not response.startswith("LLM HATA"):
|
|
@@ -281,14 +432,16 @@ def run_query(conn, system_prompt, question):
|
|
|
281
432
|
if not sql:
|
|
282
433
|
print(f"\n❌ Model SQL üretmedi:")
|
|
283
434
|
print(f" {response[:300]}")
|
|
284
|
-
|
|
435
|
+
_log("no_sql", question=question)
|
|
436
|
+
return None, None
|
|
285
437
|
|
|
286
438
|
# 3. Güvenlik kontrolü
|
|
287
439
|
safety_error = check_sql_safety(sql)
|
|
288
440
|
if safety_error:
|
|
289
441
|
print(f"\n⛔ {safety_error}")
|
|
290
442
|
print(f"🔍 SQL: {sql[:200]}")
|
|
291
|
-
|
|
443
|
+
_log("safety", question=question, sql=sql, error=safety_error)
|
|
444
|
+
return None, None
|
|
292
445
|
|
|
293
446
|
# 3b. LIKE → ILIKE guardrail
|
|
294
447
|
sql = _like_to_ilike(sql)
|
|
@@ -305,45 +458,94 @@ def run_query(conn, system_prompt, question):
|
|
|
305
458
|
|
|
306
459
|
print(f"\n📊 SONUÇ ({len(rows)} satır, {query_ms:.0f}ms)")
|
|
307
460
|
print(format_table(columns, rows))
|
|
461
|
+
if len(rows) > 50:
|
|
462
|
+
print(f" ⚠ {len(rows)} satır döndü, ilk 50 gösteriliyor. Soruyu daraltın.")
|
|
308
463
|
|
|
309
464
|
# Entity context çıkar
|
|
310
465
|
entities = _extract_entity_ids(columns, rows)
|
|
311
466
|
if entities is None and len(rows) > 100:
|
|
312
|
-
print(" ⚠
|
|
313
|
-
|
|
467
|
+
print(" ⚠ Firma referansı için soruyu daraltın (max 100 entity).")
|
|
468
|
+
_log("ok", question=question, sql=sql, row_count=len(rows),
|
|
469
|
+
query_ms=round(query_ms), columns=columns)
|
|
470
|
+
return entities, sql
|
|
314
471
|
except duckdb.Error as e:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
472
|
+
_log("error", question=question, sql=sql, error=str(e))
|
|
473
|
+
# Genel SQL hata retry — tek retry, her hata tipinde
|
|
474
|
+
print(f"🔄 Retry ({_short_error(e)})...")
|
|
475
|
+
retry_q = _build_retry_prompt(question, sql, e, col_map)
|
|
476
|
+
resp2, _, _ = ask_model(system_prompt, retry_q)
|
|
477
|
+
sql2 = extract_sql(resp2)
|
|
478
|
+
if sql2 and not check_sql_safety(sql2):
|
|
479
|
+
sql2 = _like_to_ilike(sql2)
|
|
480
|
+
print(f"🔍 Retry SQL: {sql2}")
|
|
481
|
+
try:
|
|
482
|
+
result = conn.execute(sql2)
|
|
483
|
+
columns = [desc[0] for desc in result.description]
|
|
484
|
+
rows = result.fetchall()
|
|
485
|
+
query_ms = (time.time() - t0) * 1000
|
|
486
|
+
print(f"\n📊 SONUÇ ({len(rows)} satır, {query_ms:.0f}ms)")
|
|
487
|
+
print(format_table(columns, rows))
|
|
488
|
+
entities = _extract_entity_ids(columns, rows)
|
|
489
|
+
if entities is None and len(rows) > 100:
|
|
490
|
+
print(" ⚠ Önceki sorgu çok geniş — firma referansı için soruyu daraltın.")
|
|
491
|
+
_log("retry_ok", question=question, original_sql=sql,
|
|
492
|
+
retry_sql=sql2, row_count=len(rows), columns=columns)
|
|
493
|
+
return entities, sql2
|
|
494
|
+
except duckdb.Error as e2:
|
|
495
|
+
print(f"\n❌ Retry de başarısız: {_short_error(e2)}")
|
|
496
|
+
_log("retry_fail", question=question, original_sql=sql,
|
|
497
|
+
retry_sql=sql2, error=str(e2))
|
|
498
|
+
return None, None
|
|
336
499
|
print(f"\n❌ SQL hatası: {e}")
|
|
337
500
|
print(f"🔍 SQL: {sql}")
|
|
338
|
-
return None
|
|
501
|
+
return None, None
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def handle_slash_command(cmd, state):
|
|
505
|
+
"""Slash command işle. True → normal sorgu akışına girme."""
|
|
506
|
+
cmd = cmd.strip().lower()
|
|
507
|
+
|
|
508
|
+
if cmd == "/s":
|
|
509
|
+
state["last_result_entities"] = None
|
|
510
|
+
state["last_sql"] = None
|
|
511
|
+
print("🧹 Bellek temizlendi.")
|
|
512
|
+
_log("cmd", cmd="/s")
|
|
513
|
+
return True
|
|
514
|
+
|
|
515
|
+
if cmd == "/schema":
|
|
516
|
+
print(f"\n{state['schema_text']}\n")
|
|
517
|
+
return True
|
|
518
|
+
|
|
519
|
+
if cmd == "/trace":
|
|
520
|
+
state["trace"] = not state.get("trace", False)
|
|
521
|
+
print(f"🔍 Trace: {'açık' if state['trace'] else 'kapalı'}")
|
|
522
|
+
return True
|
|
523
|
+
|
|
524
|
+
if cmd == "/help":
|
|
525
|
+
print("Komutlar:")
|
|
526
|
+
print(" /s — önceki sorgu hafızasını temizle")
|
|
527
|
+
print(" /schema — veritabanı şemasını göster")
|
|
528
|
+
print(" /trace — SQL trace modunu aç/kapa")
|
|
529
|
+
print(" /help — bu mesaj")
|
|
530
|
+
print(" quit — çıkış")
|
|
531
|
+
return True
|
|
339
532
|
|
|
533
|
+
return False
|
|
340
534
|
|
|
341
|
-
|
|
535
|
+
|
|
536
|
+
def interactive_loop(conn, schema_text, domain_text, col_map):
|
|
342
537
|
"""REPL döngüsü."""
|
|
343
|
-
print(f"\ncontsql hazır. Model: {MODEL}")
|
|
538
|
+
print(f"\ncontsql hazır. Model: {MODEL} | /help komutlar")
|
|
344
539
|
print("Çıkmak için: quit/exit/q\n")
|
|
345
540
|
|
|
346
|
-
|
|
541
|
+
column_hints = format_column_hints(col_map)
|
|
542
|
+
|
|
543
|
+
state = {
|
|
544
|
+
"last_result_entities": None,
|
|
545
|
+
"last_sql": None,
|
|
546
|
+
"trace": False,
|
|
547
|
+
"schema_text": schema_text,
|
|
548
|
+
}
|
|
347
549
|
|
|
348
550
|
while True:
|
|
349
551
|
try:
|
|
@@ -356,15 +558,27 @@ def interactive_loop(conn, schema_text, domain_text):
|
|
|
356
558
|
continue
|
|
357
559
|
if question.lower() in ("quit", "exit", "q", "çık"):
|
|
358
560
|
break
|
|
359
|
-
|
|
360
|
-
|
|
561
|
+
|
|
562
|
+
if question.startswith("/"):
|
|
563
|
+
if not handle_slash_command(question, state):
|
|
564
|
+
print(f"Bilinmeyen komut: {question}. /help yazın.")
|
|
361
565
|
continue
|
|
362
566
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
567
|
+
# SQL context uzunluk uyarısı
|
|
568
|
+
if state["last_sql"] and has_query_trigger(question) \
|
|
569
|
+
and len(state["last_sql"]) > MAX_SQL_CONTEXT_LENGTH:
|
|
570
|
+
print("⚠ Önceki sorgu çok uzun — lütfen soruyu tam yazın.")
|
|
571
|
+
|
|
572
|
+
system_prompt = build_system_prompt(schema_text, domain_text,
|
|
573
|
+
state["last_result_entities"],
|
|
574
|
+
question=question,
|
|
575
|
+
last_sql=state["last_sql"],
|
|
576
|
+
column_hints=column_hints)
|
|
577
|
+
entities, sql = run_query(conn, system_prompt, question, col_map)
|
|
366
578
|
if entities is not None:
|
|
367
|
-
last_result_entities = entities
|
|
579
|
+
state["last_result_entities"] = entities
|
|
580
|
+
if sql is not None:
|
|
581
|
+
state["last_sql"] = sql
|
|
368
582
|
print()
|
|
369
583
|
|
|
370
584
|
|
|
@@ -388,19 +602,26 @@ def main():
|
|
|
388
602
|
|
|
389
603
|
conn = duckdb.connect(str(db_path), read_only=True)
|
|
390
604
|
|
|
391
|
-
#
|
|
605
|
+
# Loglama başlat
|
|
606
|
+
log_path = _init_log(db_path)
|
|
607
|
+
|
|
608
|
+
# Schema + domain + column map
|
|
392
609
|
schema_text = read_schema(conn)
|
|
393
610
|
domain_text = read_domain_notes(str(db_path))
|
|
611
|
+
col_map = build_column_owner_map(conn)
|
|
394
612
|
|
|
395
|
-
print(f"DB: {db_path} | Model: {MODEL}")
|
|
613
|
+
print(f"DB: {db_path} | Model: {MODEL} | Log: {log_path}")
|
|
614
|
+
_log("session_start", db=str(db_path), model=MODEL)
|
|
396
615
|
|
|
397
616
|
# Tek soru veya interaktif
|
|
398
617
|
if args.question:
|
|
618
|
+
column_hints = format_column_hints(col_map)
|
|
399
619
|
system_prompt = build_system_prompt(schema_text, domain_text,
|
|
400
|
-
question=args.question
|
|
401
|
-
|
|
620
|
+
question=args.question,
|
|
621
|
+
column_hints=column_hints)
|
|
622
|
+
run_query(conn, system_prompt, args.question, col_map)
|
|
402
623
|
else:
|
|
403
|
-
interactive_loop(conn, schema_text, domain_text)
|
|
624
|
+
interactive_loop(conn, schema_text, domain_text, col_map)
|
|
404
625
|
|
|
405
626
|
conn.close()
|
|
406
627
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|