projectdavid 1.31.1__py3-none-any.whl → 1.32.1__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.

Potentially problematic release.


This version of projectdavid might be problematic. Click here for more details.

@@ -4,13 +4,19 @@ from typing import Generator, Optional
4
4
 
5
5
  from projectdavid_common import UtilsInterface
6
6
 
7
- logging_utility = UtilsInterface.LoggingUtility()
7
+ from projectdavid.utils.function_call_suppressor import FunctionCallSuppressor
8
+ from projectdavid.utils.peek_gate import PeekGate
9
+
10
+ LOG = UtilsInterface.LoggingUtility()
8
11
 
9
12
 
10
13
  class SynchronousInferenceStream:
11
14
  _GLOBAL_LOOP = asyncio.new_event_loop()
12
15
  asyncio.set_event_loop(_GLOBAL_LOOP)
13
16
 
17
+ # --------------------------------------------------------------
18
+ # ctor / setup
19
+ # --------------------------------------------------------------
14
20
  def __init__(self, inference) -> None:
15
21
  self.inference_client = inference
16
22
  self.user_id: Optional[str] = None
@@ -36,31 +42,23 @@ class SynchronousInferenceStream:
36
42
  self.run_id = run_id
37
43
  self.api_key = api_key
38
44
 
45
+ # --------------------------------------------------------------
46
+ # main streaming entry-point
47
+ # --------------------------------------------------------------
39
48
  def stream_chunks(
40
49
  self,
41
50
  provider: str,
42
51
  model: str,
43
- *, # Following parameters are keyword-only.
52
+ *,
44
53
  api_key: Optional[str] = None,
45
54
  timeout_per_chunk: float = 280.0,
55
+ suppress_fc: bool = True,
46
56
  ) -> Generator[dict, None, None]:
47
- """
48
- Streams inference response chunks synchronously by wrapping an async generator.
49
-
50
- Args:
51
- provider (str): The provider name.
52
- model (str): The model name.
53
- api_key (Optional[str]): API key for authentication. If not provided, falls back to the value from setup().
54
- timeout_per_chunk (float): Timeout per chunk in seconds.
55
-
56
- Yields:
57
- dict: A chunk of the inference response.
58
- """
59
- # Prefer direct input over self.api_key
57
+
60
58
  resolved_api_key = api_key or self.api_key
61
59
 
62
- async def _stream_chunks_async() -> Generator[dict, None, None]:
63
- async for chunk in self.inference_client.stream_inference_response(
60
+ async def _stream_chunks_async():
61
+ async for chk in self.inference_client.stream_inference_response(
64
62
  provider=provider,
65
63
  model=model,
66
64
  api_key=resolved_api_key,
@@ -69,30 +67,57 @@ class SynchronousInferenceStream:
69
67
  run_id=self.run_id,
70
68
  assistant_id=self.assistant_id,
71
69
  ):
72
- yield chunk
70
+ yield chk
71
+
72
+ agen = _stream_chunks_async().__aiter__()
73
+
74
+ # ---------- suppression chain ----------
75
+ if suppress_fc:
76
+ _suppressor = FunctionCallSuppressor()
77
+ _peek_gate = PeekGate(_suppressor)
78
+
79
+ def _filter_text(txt: str) -> str:
80
+ return _peek_gate.feed(txt)
81
+
82
+ else:
73
83
 
74
- gen = _stream_chunks_async().__aiter__()
84
+ def _filter_text(txt: str) -> str: # no-op
85
+ return txt
86
+
87
+ # ---------------------------------------
75
88
 
76
89
  while True:
77
90
  try:
78
91
  chunk = self._GLOBAL_LOOP.run_until_complete(
79
- asyncio.wait_for(gen.__anext__(), timeout=timeout_per_chunk)
92
+ asyncio.wait_for(agen.__anext__(), timeout=timeout_per_chunk)
80
93
  )
94
+
95
+ # provider-labelled function_call
96
+ if suppress_fc and chunk.get("type") == "function_call":
97
+ LOG.debug("[SUPPRESSOR] blocked provider-labelled function_call")
98
+ continue
99
+
100
+ # inline content
101
+ if isinstance(chunk.get("content"), str):
102
+ chunk["content"] = _filter_text(chunk["content"])
103
+ if chunk["content"] == "":
104
+ continue # fully suppressed (or still peeking)
105
+
81
106
  yield chunk
107
+
82
108
  except StopAsyncIteration:
83
- logging_utility.info("Stream completed normally.")
109
+ LOG.info("Stream completed normally.")
84
110
  break
85
111
  except asyncio.TimeoutError:
86
- logging_utility.error(
87
- "[TimeoutError] Timeout occurred, stopping stream."
88
- )
112
+ LOG.error("[TimeoutError] Timeout occurred, stopping stream.")
89
113
  break
90
114
  except Exception as e:
91
- logging_utility.error(
92
- "Unexpected error during streaming completions: %s", e
93
- )
115
+ LOG.error("Unexpected error during streaming completions: %s", e)
94
116
  break
95
117
 
118
+ # --------------------------------------------------------------
119
+ # housekeeping
120
+ # --------------------------------------------------------------
96
121
  @classmethod
97
122
  def shutdown_loop(cls) -> None:
98
123
  if cls._GLOBAL_LOOP and not cls._GLOBAL_LOOP.is_closed():
@@ -0,0 +1,40 @@
1
+ # ------------------------------------------------------------------
2
+ # utils.function_call_suppressor (unchanged except for logger)
3
+ # ------------------------------------------------------------------
4
+ import re
5
+
6
+ from projectdavid_common.utilities.logging_service import LoggingUtility
7
+
8
+ LOG = LoggingUtility()
9
+
10
+
11
+ class FunctionCallSuppressor:
12
+ OPEN_RE = re.compile(r"<\s*fc\s*>", re.I)
13
+ CLOSE_RE = re.compile(r"</\s*fc\s*>", re.I)
14
+
15
+ def __init__(self):
16
+ self.in_fc = False
17
+ self.buf = ""
18
+
19
+ def filter_chunk(self, txt: str) -> str:
20
+ self.buf += txt
21
+ out = ""
22
+ while self.buf:
23
+ if not self.in_fc:
24
+ m = self.OPEN_RE.search(self.buf)
25
+ if not m:
26
+ out += self.buf
27
+ self.buf = ""
28
+ break
29
+ out += self.buf[: m.start()]
30
+ LOG.debug("[SUPPRESSOR] <fc> detected")
31
+ self.buf = self.buf[m.end() :]
32
+ self.in_fc = True
33
+ else:
34
+ m = self.CLOSE_RE.search(self.buf)
35
+ if not m:
36
+ break
37
+ LOG.debug("[SUPPRESSOR] </fc> detected — block suppressed")
38
+ self.buf = self.buf[m.end() :]
39
+ self.in_fc = False
40
+ return out
@@ -0,0 +1,71 @@
1
+ # ------------------------------------------------------------------
2
+ # utils.peek_gate (progressive flush version)
3
+ # ------------------------------------------------------------------
4
+ import re
5
+
6
+ from projectdavid_common.utilities.logging_service import LoggingUtility
7
+
8
+ from .function_call_suppressor import FunctionCallSuppressor
9
+
10
+ LOG = LoggingUtility()
11
+
12
+
13
+ class PeekGate:
14
+ """
15
+ • Streams *most* text immediately while the gate decides whether
16
+ a <fc> block will appear.
17
+ • Keeps only SAFETY_MARGIN bytes back so a tag split across two
18
+ chunks can still be caught.
19
+ """
20
+
21
+ SAFETY_MARGIN = 8 # enough to hold "<fc>" + whitespace
22
+
23
+ def __init__(self, downstream: FunctionCallSuppressor, peek_limit: int = 2048):
24
+ self.downstream = downstream
25
+ self.peek_limit = peek_limit
26
+ self.buf = ""
27
+ self.mode = "peeking" # → "normal" after decision
28
+ self.suppressing = False
29
+
30
+ # ----------------------------------------------------------
31
+ def _safe_flush(self, new_txt: str = "") -> str:
32
+ """
33
+ Return everything except the last SAFETY_MARGIN chars,
34
+ which are kept for tag-boundary safety.
35
+ """
36
+ self.buf += new_txt
37
+ if len(self.buf) <= self.SAFETY_MARGIN:
38
+ return ""
39
+ flush_len = len(self.buf) - self.SAFETY_MARGIN
40
+ head, self.buf = self.buf[:flush_len], self.buf[flush_len:]
41
+ return head
42
+
43
+ # ----------------------------------------------------------
44
+ def feed(self, txt: str) -> str:
45
+ # decision already taken
46
+ if self.mode == "normal":
47
+ return self.downstream.filter_chunk(txt) if self.suppressing else txt
48
+
49
+ # still peeking …
50
+ self.buf += txt
51
+ m = re.search(r"<\s*fc\s*>", self.buf, flags=re.I)
52
+ if m: # tag found
53
+ head = self.buf[: m.start()]
54
+ LOG.debug("[PEEK] <fc> located – engaging suppressor")
55
+ self.suppressing = True
56
+ self.mode = "normal"
57
+ tail, self.buf = self.buf[m.start() :], ""
58
+ return head + self.downstream.filter_chunk(tail)
59
+
60
+ # no tag yet – overflow?
61
+ if len(self.buf) >= self.peek_limit:
62
+ LOG.debug(
63
+ "[PEEK] no <fc> tag within first %d chars – " "streaming normally",
64
+ self.peek_limit,
65
+ )
66
+ flush, self.mode = self.buf, "normal"
67
+ self.buf = ""
68
+ return flush
69
+
70
+ # still undecided → flush safe part
71
+ return self._safe_flush()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: projectdavid
3
- Version: 1.31.1
3
+ Version: 1.32.1
4
4
  Summary: Python SDK for interacting with the Entities Assistant API.
5
5
  Author-email: Francis Neequaye Armah <francis.neequaye@projectdavid.co.uk>
6
6
  License: PolyForm Noncommercial License 1.0.0
@@ -15,7 +15,7 @@ projectdavid/clients/files_client.py,sha256=XkIDzbQFGDrd88taf0Kouc_4YJOPIYEHiIyW
15
15
  projectdavid/clients/inference_client.py,sha256=xz4ACPv5Tkis604QxO5mJX1inH_TGDfQP-31geETYpE,6609
16
16
  projectdavid/clients/messages_client.py,sha256=467xeIt3VYs6cG8-bl-eDRi_auWOPmfd5tSJDmQSJUI,17232
17
17
  projectdavid/clients/runs.py,sha256=-fXOq5L9w2efDPmZkNxb0s2yjl6oN0XN4_aLXqaeceo,25270
18
- projectdavid/clients/synchronous_inference_wrapper.py,sha256=25dNSel5DVieu2aFg6G1e4ENo5WWB4EB8_jugIiVzlo,3433
18
+ projectdavid/clients/synchronous_inference_wrapper.py,sha256=T5OlM7eQYZNuLzAOADyKDtWVst5oEPEtENNJ0H-6kXI,4299
19
19
  projectdavid/clients/threads_client.py,sha256=ekzU5w14zftmtmFkiec3NC90Of-_KVSUY1qH9cmfSFg,6771
20
20
  projectdavid/clients/tools_client.py,sha256=GkCVOmwpAoPqVt6aYmH0G1HIFha3iEwR9IIf9teR0j8,11487
21
21
  projectdavid/clients/users_client.py,sha256=eCuUb9qvyH1GUFhZu6TRL9zdoK-qzHSs8-Vmrk_0mmg,13729
@@ -29,11 +29,13 @@ projectdavid/synthesis/prompt.py,sha256=Jlya6avnWV5ryaJaibt4W29XYxRlJRV6rwHfjgYy
29
29
  projectdavid/synthesis/reranker.py,sha256=aFjdLDoqnhgzxPIoRfs4y_Z_F6T88g5Sg8R5m0HMxdU,868
30
30
  projectdavid/synthesis/retriever.py,sha256=8R5HIgPXMKIQxV5KsN8u_gVVP4JY-wsDwJd92A-74-o,2300
31
31
  projectdavid/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ projectdavid/utils/function_call_suppressor.py,sha256=LsDhySVH0kS8nydPBrHrOtT30yhVvwU24QHApGu1P34,1305
32
33
  projectdavid/utils/monitor_launcher.py,sha256=3YAgJdeuaUvq3JGvpA4ymqFsAnk29nH5q93cwStP4hc,2836
34
+ projectdavid/utils/peek_gate.py,sha256=5whMRnDOQjATRpThWDJkvY9ScXuJ7Sd_-9rvGgXeTAQ,2532
33
35
  projectdavid/utils/run_monitor.py,sha256=F_WkqIP-qnWH-4llIbileWWLfRj2Q1Cg-ni23SR1rec,3786
34
36
  projectdavid/utils/vector_search_formatter.py,sha256=YTe3HPGec26qGY7uxY8_GS8lc4QaN6aNXMzkl29nZpI,1735
35
- projectdavid-1.31.1.dist-info/licenses/LICENSE,sha256=_8yjiEGttpS284BkfhXxfERqTRZW_tUaHiBB0GTJTMg,4563
36
- projectdavid-1.31.1.dist-info/METADATA,sha256=f-SkJ06HipWaVJZ0W-bECBP7-2OjCNqTNc58kN7A0qw,10781
37
- projectdavid-1.31.1.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
38
- projectdavid-1.31.1.dist-info/top_level.txt,sha256=kil8GU4s7qYRfNnzGnFHhZnSNRSxgNG-J4HLgQMmMtw,13
39
- projectdavid-1.31.1.dist-info/RECORD,,
37
+ projectdavid-1.32.1.dist-info/licenses/LICENSE,sha256=_8yjiEGttpS284BkfhXxfERqTRZW_tUaHiBB0GTJTMg,4563
38
+ projectdavid-1.32.1.dist-info/METADATA,sha256=IYDFVAHpMpR3Ly2-SHiPE7HN1Iaz1mayP2ihGqtjHgo,10781
39
+ projectdavid-1.32.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
40
+ projectdavid-1.32.1.dist-info/top_level.txt,sha256=kil8GU4s7qYRfNnzGnFHhZnSNRSxgNG-J4HLgQMmMtw,13
41
+ projectdavid-1.32.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5