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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contsql
3
- Version: 0.2.0
3
+ Version: 0.2.3
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: duckdb
6
6
  Requires-Dist: requests
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contsql
3
- Version: 0.2.0
3
+ Version: 0.2.3
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: duckdb
6
6
  Requires-Dist: requests
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- # v0.2 | 2026-04-13 | case insensitive ILIKE guardrail + önceki sorgu entity context
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
- "Kullanıcı 'bu firmalar', 'bunların', 'aynıları', 'yukarıdakiler' gibi "
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
- last_result_entities = None
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
- if question.lower() in ("schema", "şema"):
296
- print(f"\n{read_schema(conn)}\n")
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, last_result_entities)
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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "contsql"
7
- version = "0.2.0"
7
+ version = "0.2.3"
8
8
  requires-python = ">=3.10"
9
9
  dependencies = ["duckdb", "requests"]
10
10
 
File without changes
File without changes