quillsql 2.2.8__py3-none-any.whl → 2.2.10__py3-none-any.whl

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.
quillsql/core.py CHANGED
@@ -1,5 +1,8 @@
1
1
  import os
2
2
  import codecs
3
+ import uuid
4
+ from datetime import date, datetime
5
+ from decimal import Decimal
3
6
  from dotenv import load_dotenv
4
7
 
5
8
  import requests
@@ -393,6 +396,8 @@ class Quill:
393
396
  tenants,
394
397
  metadata,
395
398
  flags=None,
399
+ filters=None,
400
+ admin_enabled=None,
396
401
  ):
397
402
  if not tenants:
398
403
  raise ValueError("You may not pass an empty tenants array.")
@@ -409,51 +414,26 @@ class Quill:
409
414
  try:
410
415
  # Set tenant IDs in the connection
411
416
  self.target_connection.tenant_ids = extract_tenant_ids(tenants)
417
+ if task in ("chat", "agent"):
418
+ for event in self._agentic_chat_loop(
419
+ tenants,
420
+ metadata,
421
+ flags,
422
+ filters,
423
+ admin_enabled,
424
+ ):
425
+ yield event
426
+ return
412
427
 
413
- # Handle tenant flags synthesis
414
- tenant_flags = None
415
- if tenants[0] == SINGLE_TENANT and flags:
416
- if flags and isinstance(flags[0], dict):
417
- tenant_flags = [{'tenantField': SINGLE_TENANT, 'flags': flags}]
418
- else:
419
- tenant_flags = flags
420
-
421
- payload = {
422
- **metadata,
423
- "tenants": tenants,
424
- "flags": tenant_flags,
425
- }
426
- # Custom JSON Encoder to handle Enums
427
- class EnumEncoder(json.JSONEncoder):
428
- def default(self, obj):
429
- if isinstance(obj, Enum):
430
- return obj.value # Convert enum to its value (string in this case)
431
- return super().default(obj)
432
- url = f"{self.baseUrl}/sdk/{task}"
433
- headers = {"Authorization": f"Bearer {self.private_key}", "Content-Type": "application/json","Accept": "text/event-stream"}
434
- encoded = json.dumps(payload, cls=EnumEncoder)
435
-
436
- resp = requests.post(url, data=encoded, headers=headers, stream=True)
437
- decoder = codecs.getincrementaldecoder('utf-8')()
438
- buf = ""
439
- for chunk in resp.iter_content(chunk_size=4096):
440
- buf += decoder.decode(chunk)
441
- while "\n\n" in buf:
442
- raw_event, buf = buf.split("\n\n", 1)
443
- data_lines = []
444
- for line in raw_event.splitlines():
445
- if line.startswith("data:"):
446
- data_lines.append(line[len("data:"):].strip())
447
- if not data_lines:
448
- continue
449
- payload = "\n".join(data_lines)
450
- if payload == "[DONE]":
451
- break
452
- yield json.loads(payload)
453
-
454
- # flush any partial code points at the end
455
- buf += decoder.decode(b"", final=True)
456
- yield buf
428
+ for event in self._stream_sse(
429
+ task,
430
+ tenants,
431
+ metadata,
432
+ flags,
433
+ filters,
434
+ admin_enabled,
435
+ ):
436
+ yield event
457
437
  return
458
438
  except Exception as err:
459
439
  yield {
@@ -462,6 +442,228 @@ class Quill:
462
442
  }
463
443
  return
464
444
 
445
+ def _normalize_tenant_flags(self, tenants, flags):
446
+ tenant_flags = None
447
+ if tenants and tenants[0] == SINGLE_TENANT and flags:
448
+ if flags and isinstance(flags[0], dict):
449
+ tenant_flags = [{"tenantField": SINGLE_TENANT, "flags": flags}]
450
+ else:
451
+ tenant_flags = flags
452
+ return tenant_flags
453
+
454
+ def _agentic_chat_loop(self, tenants, metadata, flags, filters, admin_enabled):
455
+ messages = list(metadata.get("messages") or [])
456
+ max_iterations = 10
457
+
458
+ for _ in range(max_iterations):
459
+ payload = {
460
+ **metadata,
461
+ "messages": messages,
462
+ }
463
+
464
+ has_tool_calls = False
465
+ assistant_text = ""
466
+ tool_results = []
467
+ tool_calls = []
468
+
469
+ for event in self._stream_sse(
470
+ "agent",
471
+ tenants,
472
+ payload,
473
+ flags,
474
+ filters,
475
+ admin_enabled,
476
+ ):
477
+ yield event
478
+
479
+ if event.get("type") == "text-delta":
480
+ assistant_text += event.get("delta", "")
481
+
482
+ if event.get("type") == "tool-input-available":
483
+ tool_name = event.get("toolName")
484
+ tool_call_id = event.get("toolCallId")
485
+ tool_input = event.get("input") or {}
486
+
487
+ if tool_call_id is None:
488
+ yield {
489
+ "type": "error",
490
+ "errorText": "Missing toolCallId for tool-input-available event.",
491
+ }
492
+ continue
493
+
494
+ has_tool_calls = True
495
+ yield {
496
+ "type": "tool-executing",
497
+ "toolCallId": tool_call_id,
498
+ "toolName": tool_name,
499
+ }
500
+
501
+ result = self._execute_tool_locally(tool_name, tool_input)
502
+ tool_results.append(
503
+ {
504
+ "toolCallId": tool_call_id,
505
+ "toolName": tool_name,
506
+ "input": tool_input,
507
+ "result": result,
508
+ }
509
+ )
510
+ tool_calls.append(
511
+ {
512
+ "id": tool_call_id,
513
+ "type": "function",
514
+ "function": {
515
+ "name": tool_name,
516
+ "arguments": json.dumps(tool_input or {}),
517
+ },
518
+ }
519
+ )
520
+
521
+ yield {
522
+ "type": "tool-result",
523
+ "toolCallId": tool_call_id,
524
+ "result": result,
525
+ }
526
+
527
+ if event.get("type") in ("finish", "error"):
528
+ break
529
+
530
+ if not has_tool_calls:
531
+ break
532
+
533
+ def _build_tool_result_for_history(tool_result):
534
+ result = tool_result.get("result") or {}
535
+ has_rows = isinstance(result.get("rows"), list)
536
+ has_fields = isinstance(result.get("fields"), list)
537
+ is_query_result = has_rows or has_fields or result.get("dbMismatched")
538
+ if not is_query_result:
539
+ return result
540
+ tool_input = tool_result.get("input") or {}
541
+ error = result.get("error") or tool_input.get("error")
542
+ status = "error" if error or result.get("dbMismatched") else "success"
543
+ payload = {"status": status}
544
+ if tool_input.get("sql"):
545
+ payload["sql"] = tool_input.get("sql")
546
+ if error:
547
+ payload["error"] = error
548
+ if result.get("dbMismatched"):
549
+ payload["meta"] = {
550
+ "dbMismatched": True,
551
+ "backendDatabaseType": result.get("backendDatabaseType"),
552
+ }
553
+ elif not error:
554
+ payload["meta"] = {
555
+ "rowsFetchedSuccessfully": True,
556
+ "rowCount": len(result.get("rows") or []),
557
+ }
558
+ return payload
559
+
560
+ messages.append(
561
+ {
562
+ "role": "assistant",
563
+ "content": assistant_text or None,
564
+ "tool_calls": tool_calls,
565
+ }
566
+ )
567
+ for tool_result in tool_results:
568
+ messages.append(
569
+ {
570
+ "role": "tool",
571
+ "tool_call_id": tool_result["toolCallId"],
572
+ "content": json.dumps(_build_tool_result_for_history(tool_result)),
573
+ }
574
+ )
575
+
576
+ yield {"type": "done"}
577
+
578
+ def _execute_tool_locally(self, tool_name, tool_input):
579
+ if tool_name == "generateReport":
580
+ if tool_input.get("error"):
581
+ return {"error": tool_input.get("error")}
582
+ sql = tool_input.get("sql")
583
+ if not sql:
584
+ return {"error": "No SQL provided"}
585
+ results = self.run_queries(
586
+ [sql],
587
+ self.target_connection.database_type,
588
+ )
589
+ if results.get("dbMismatched"):
590
+ return results
591
+ query_results = results.get("queryResults") or []
592
+ if query_results and isinstance(query_results[0], dict):
593
+ if query_results[0].get("error"):
594
+ return query_results[0]
595
+ return {
596
+ "rows": query_results[0].get("rows", []),
597
+ "fields": query_results[0].get("fields", []),
598
+ }
599
+ return {"rows": [], "fields": []}
600
+
601
+ if tool_name == "createChart":
602
+ return {"chartConfig": tool_input}
603
+
604
+ return {"error": f"Unknown tool: {tool_name}"}
605
+
606
+ def _json_default(self, obj):
607
+ if isinstance(obj, Enum):
608
+ return obj.value
609
+ if isinstance(obj, uuid.UUID):
610
+ return str(obj)
611
+ if isinstance(obj, (datetime, date)):
612
+ return obj.isoformat()
613
+ if isinstance(obj, Decimal):
614
+ return float(obj)
615
+ raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
616
+
617
+ def _stream_sse(self, endpoint, tenants, payload, flags, filters, admin_enabled):
618
+ tenant_flags = self._normalize_tenant_flags(tenants, flags)
619
+ request_payload = {
620
+ **payload,
621
+ "tenants": tenants,
622
+ "flags": tenant_flags,
623
+ }
624
+ if filters:
625
+ request_payload["sdkFilters"] = [convert_custom_filter(f) for f in filters]
626
+ if admin_enabled is not None:
627
+ request_payload["adminEnabled"] = admin_enabled
628
+
629
+ url = f"{self.baseUrl}/sdk/{endpoint}"
630
+ headers = {
631
+ "Authorization": f"Bearer {self.private_key}",
632
+ "Content-Type": "application/json",
633
+ "Accept": "text/event-stream",
634
+ }
635
+ encoded = json.dumps(request_payload, default=self._json_default)
636
+
637
+ resp = requests.post(url, data=encoded, headers=headers, stream=True)
638
+ decoder = codecs.getincrementaldecoder("utf-8")()
639
+ buf = ""
640
+ for chunk in resp.iter_content(chunk_size=4096):
641
+ buf += decoder.decode(chunk)
642
+ while "\n\n" in buf:
643
+ raw_event, buf = buf.split("\n\n", 1)
644
+ data_lines = []
645
+ for line in raw_event.splitlines():
646
+ if line.startswith("data:"):
647
+ data_lines.append(line[len("data:"):].strip())
648
+ if not data_lines:
649
+ continue
650
+ payload = "\n".join(data_lines)
651
+ if payload == "[DONE]":
652
+ return
653
+ try:
654
+ parsed = json.loads(payload)
655
+ if isinstance(parsed, str):
656
+ yield {"type": "text-delta", "id": "0", "delta": parsed}
657
+ else:
658
+ yield parsed
659
+ except json.JSONDecodeError:
660
+ continue
661
+
662
+ # flush any partial code points at the end
663
+ buf += decoder.decode(b"", final=True)
664
+ yield buf
665
+ return
666
+
465
667
  def apply_limit(self, query, limit):
466
668
  # Simple logic: if query already has a limit, don't add another
467
669
  if getattr(self.target_connection, 'database_type', '').lower() == 'mssql':
@@ -633,16 +835,9 @@ class Quill:
633
835
  return results
634
836
 
635
837
  def post_quill(self, path, payload):
636
- # Custom JSON Encoder to handle Enums
637
- class EnumEncoder(json.JSONEncoder):
638
- def default(self, obj):
639
- if isinstance(obj, Enum):
640
- return obj.value # Convert enum to its value (string in this case)
641
- return super().default(obj)
642
-
643
838
  url = f"{self.baseUrl}/sdk/{path}"
644
839
  # Set content type to application/json
645
840
  headers = {"Authorization": f"Bearer {self.private_key}", "Content-Type": "application/json"}
646
- encoded = json.dumps(payload, cls=EnumEncoder)
841
+ encoded = json.dumps(payload, default=self._json_default)
647
842
  response = requests.post(url, data=encoded, headers=headers)
648
843
  return response.json()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quillsql
3
- Version: 2.2.8
3
+ Version: 2.2.10
4
4
  Summary: Quill SDK for Python.
5
5
  Home-page: https://github.com/quill-sql/quill-python
6
6
  Author: Quill
@@ -1,5 +1,5 @@
1
1
  quillsql/__init__.py,sha256=wjJfszle5vheUbgUfJMHQqtqhx2W3UaDN4ndcRIfmkQ,236
2
- quillsql/core.py,sha256=yLDGW_BZTcY5FBxJVefCxzPBgzChy_vLHfJNylz6wuo,26737
2
+ quillsql/core.py,sha256=cxuAfOgQ3AMt26hQgNTFejGv7L_fEAVXtV7dsNdiT7k,33592
3
3
  quillsql/error.py,sha256=n9VKHw4FAgg7ZEAz2YQ8L_8FdRG_1shwGngf2iWhUSM,175
4
4
  quillsql/assets/__init__.py,sha256=oXQ2ZS5XDXkXTYjADxNfGt55cIn_rqfgWL2EDqjTyoI,45
5
5
  quillsql/assets/pgtypes.py,sha256=-B_2wUaoAsdX7_HnJhUlx4ptZQ6x-cXwuST9ACgGFdE,33820
@@ -15,7 +15,7 @@ quillsql/utils/post_quill_executor.py,sha256=DB1RHNfqHPYarMM10vSv--UjpCZqe4qYTjq
15
15
  quillsql/utils/run_query_processes.py,sha256=QwnMr5UwXdtO_W88lv5nBaf6pJ_h5oWQnYd8K9oHQ5s,1030
16
16
  quillsql/utils/schema_conversion.py,sha256=TFfMibN9nOsxNRhHw5YIFl3jGTvipG81bxX4LFDulUY,314
17
17
  quillsql/utils/tenants.py,sha256=ZD2FuKz0gjBVSsThHDv1P8PU6EL8E009NWihE5hAH-Q,2022
18
- quillsql-2.2.8.dist-info/METADATA,sha256=qfMIakgsSk045GZzk7sNtLu-2gVIQ82toNiAp3lwif0,3052
19
- quillsql-2.2.8.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
20
- quillsql-2.2.8.dist-info/top_level.txt,sha256=eU2vHnVqwpYQJ3ADl1Q-DIBzbYejZRUhcMdN_4zMCz8,9
21
- quillsql-2.2.8.dist-info/RECORD,,
18
+ quillsql-2.2.10.dist-info/METADATA,sha256=LiIaD8DgC3gUuUTkW5Opgk2Sh5alIdVh-2UzhIJxAHQ,3053
19
+ quillsql-2.2.10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
+ quillsql-2.2.10.dist-info/top_level.txt,sha256=eU2vHnVqwpYQJ3ADl1Q-DIBzbYejZRUhcMdN_4zMCz8,9
21
+ quillsql-2.2.10.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5