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