interfaze 1.0.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.
interfaze/_stream.py ADDED
@@ -0,0 +1,323 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Tuple
6
+
7
+ from openai import AsyncOpenAI, OpenAI
8
+ from openai.lib.streaming.chat import ChatCompletionStreamEvent, ChatCompletionStreamState
9
+ from openai.types.chat import ChatCompletionChunk
10
+
11
+ from ._errors import InterfazeError
12
+ from ._types import InterfazeChatCompletion
13
+
14
+
15
+ def _tag(tag: str) -> "re.Pattern[str]":
16
+ return re.compile(rf"<{tag}>([\s\S]*?)</{tag}>")
17
+
18
+
19
+ def strip_side_channels(content: str) -> Tuple[str, Optional[str], Optional[List[Dict[str, Any]]]]:
20
+ """Pull ``<think>``/``<precontext>`` blocks out of streamed content; return the rest as text."""
21
+ think_re, pre_re = _tag("think"), _tag("precontext")
22
+ thinks = [m.strip() for m in think_re.findall(content)]
23
+ text = pre_re.sub("", think_re.sub("", content))
24
+ pre: List[Dict[str, Any]] = []
25
+ for block in pre_re.findall(content):
26
+ try:
27
+ parsed = json.loads(block.strip())
28
+ pre.extend(parsed if isinstance(parsed, list) else [parsed])
29
+ except (ValueError, TypeError):
30
+ pass
31
+ return text.strip(), ("\n".join(thinks) if thinks else None), (pre or None)
32
+
33
+
34
+ _FENCE = re.compile(r"^```(?:json)?\s*|\s*```$", re.IGNORECASE)
35
+
36
+
37
+ def strip_json_fence(content: str) -> str:
38
+ """Interfaze wraps ``json_object`` content in a ```json fence; unwrap it."""
39
+ t = content.strip()
40
+ if not t.startswith("```"):
41
+ return content
42
+ return _FENCE.sub("", t).strip()
43
+
44
+
45
+ _SIDE_OPEN = ("<think>", "<precontext>")
46
+ _SIDE_CLOSE = {"<think>": "</think>", "<precontext>": "</precontext>"}
47
+
48
+
49
+ def _suffix_prefix_len(s: str, tag: str) -> int:
50
+ for k in range(min(len(s), len(tag) - 1), 0, -1):
51
+ if s[-k:] == tag[:k]:
52
+ return k
53
+ return 0
54
+
55
+
56
+ class _SideChannelFilter:
57
+ """Incrementally strips ``<think>``/``<precontext>`` blocks, buffering tags split across chunks."""
58
+
59
+ def __init__(self) -> None:
60
+ self._buf = ""
61
+ self._close: Optional[str] = None
62
+
63
+ def feed(self, text: str) -> str:
64
+ self._buf += text
65
+ out: List[str] = []
66
+ while self._buf:
67
+ if self._close is None:
68
+ lt = self._buf.find("<")
69
+ if lt == -1:
70
+ out.append(self._buf)
71
+ self._buf = ""
72
+ break
73
+ if lt:
74
+ out.append(self._buf[:lt])
75
+ self._buf = self._buf[lt:]
76
+ opened = next((t for t in _SIDE_OPEN if self._buf.startswith(t)), None)
77
+ if opened:
78
+ self._close = _SIDE_CLOSE[opened]
79
+ self._buf = self._buf[len(opened) :]
80
+ continue
81
+ if any(t.startswith(self._buf) for t in _SIDE_OPEN):
82
+ break
83
+ out.append("<")
84
+ self._buf = self._buf[1:]
85
+ else:
86
+ end = self._buf.find(self._close)
87
+ if end == -1:
88
+ keep = _suffix_prefix_len(self._buf, self._close)
89
+ self._buf = self._buf[len(self._buf) - keep :] if keep else ""
90
+ break
91
+ self._buf = self._buf[end + len(self._close) :]
92
+ self._close = None
93
+ return "".join(out)
94
+
95
+ def flush(self) -> str:
96
+ if self._close is not None:
97
+ self._buf = ""
98
+ return ""
99
+ rest, self._buf = self._buf, ""
100
+ return rest
101
+
102
+
103
+ def _clean_for_events(
104
+ chunk: ChatCompletionChunk, filt: _SideChannelFilter, role_injected: bool
105
+ ) -> "Tuple[ChatCompletionChunk, bool]":
106
+ """Clean a chunk for openai's event state: strip side channels from content, inject a role."""
107
+ cleaned = chunk.model_copy(deep=True)
108
+ if not cleaned.choices:
109
+ return cleaned, role_injected
110
+ delta = cleaned.choices[0].delta
111
+ if not role_injected:
112
+ role_injected = True
113
+ if not delta.role:
114
+ delta.role = "assistant"
115
+ piece = filt.feed(delta.content) if isinstance(delta.content, str) else ""
116
+ if cleaned.choices[0].finish_reason:
117
+ piece += filt.flush()
118
+ if isinstance(delta.content, str) or piece:
119
+ delta.content = piece
120
+ return cleaned, role_injected
121
+
122
+
123
+ class _State:
124
+ def __init__(self) -> None:
125
+ self.content = ""
126
+ self.role: Optional[str] = None
127
+ self.finish: Optional[str] = None
128
+ self.id = ""
129
+ self.model = ""
130
+ self.created = 0
131
+ self.tool_calls: Dict[int, Dict[str, str]] = {}
132
+ self.usage: Any = None
133
+ self.system_fingerprint: Optional[str] = None
134
+
135
+ def accumulate(self, chunk: ChatCompletionChunk) -> None:
136
+ if not self.id and chunk.id:
137
+ self.id = chunk.id
138
+ if not self.model and chunk.model:
139
+ self.model = chunk.model
140
+ if not self.created and chunk.created:
141
+ self.created = chunk.created
142
+ if chunk.usage:
143
+ self.usage = chunk.usage
144
+ if chunk.system_fingerprint:
145
+ self.system_fingerprint = chunk.system_fingerprint
146
+ if not chunk.choices:
147
+ return
148
+ choice = chunk.choices[0]
149
+ delta = choice.delta
150
+ if delta and delta.role:
151
+ self.role = delta.role
152
+ if delta and isinstance(delta.content, str):
153
+ self.content += delta.content
154
+ if choice.finish_reason:
155
+ self.finish = choice.finish_reason
156
+ for tc in (delta.tool_calls or []) if delta else []:
157
+ acc = self.tool_calls.setdefault(tc.index, {"id": "", "name": "", "arguments": ""})
158
+ if tc.id:
159
+ acc["id"] = tc.id
160
+ if tc.function and tc.function.name:
161
+ acc["name"] = tc.function.name
162
+ if tc.function and tc.function.arguments:
163
+ acc["arguments"] += tc.function.arguments
164
+
165
+ def build(self, strip_fence: bool = False) -> InterfazeChatCompletion:
166
+ text, reasoning, precontext = strip_side_channels(self.content)
167
+ if strip_fence:
168
+ text = strip_json_fence(text)
169
+ tool_calls = [
170
+ {"id": t["id"], "type": "function", "function": {"name": t["name"], "arguments": t["arguments"]}}
171
+ for t in self.tool_calls.values()
172
+ ]
173
+ message: Dict[str, Any] = {"role": self.role or "assistant", "content": None if tool_calls else text}
174
+ if tool_calls:
175
+ message["tool_calls"] = tool_calls
176
+ data: Dict[str, Any] = {
177
+ "id": self.id or "",
178
+ "object": "chat.completion",
179
+ "created": self.created or 0,
180
+ "model": self.model or "interfaze-beta",
181
+ "choices": [
182
+ {"index": 0, "message": message, "finish_reason": self.finish or "stop", "logprobs": None}
183
+ ],
184
+ "vcache": False,
185
+ }
186
+ if self.usage is not None:
187
+ data["usage"] = self.usage.model_dump()
188
+ if self.system_fingerprint:
189
+ data["system_fingerprint"] = self.system_fingerprint
190
+ if reasoning:
191
+ data["reasoning"] = reasoning
192
+ if precontext:
193
+ data["precontext"] = precontext
194
+ return InterfazeChatCompletion.model_validate(data)
195
+
196
+
197
+ class InterfazeStream:
198
+ """Sync streaming helper — iterate OpenAI-style events, then ``get_final_completion()``."""
199
+
200
+ def __init__(self, client: OpenAI, kwargs: Dict[str, Any], strip_fence: bool = False) -> None:
201
+ self._client = client
202
+ self._kwargs = kwargs
203
+ self._strip_fence = strip_fence
204
+ self._state = _State()
205
+ self._openai_state: ChatCompletionStreamState[None] = ChatCompletionStreamState()
206
+ self._filter = _SideChannelFilter()
207
+ self._role_injected = False
208
+ self._started = False
209
+ self._done = False
210
+
211
+ def __enter__(self) -> "InterfazeStream":
212
+ return self
213
+
214
+ def __exit__(self, *exc: Any) -> None:
215
+ return None
216
+
217
+ def __iter__(self) -> "Iterator[ChatCompletionStreamEvent[None]]":
218
+ if self._started:
219
+ raise InterfazeError("This stream has already been consumed.")
220
+ self._started = True
221
+ for chunk in self._client.chat.completions.create(stream=True, **self._kwargs):
222
+ self._state.accumulate(chunk)
223
+ cleaned, self._role_injected = _clean_for_events(chunk, self._filter, self._role_injected)
224
+ yield from self._openai_state.handle_chunk(cleaned)
225
+ self._done = True
226
+
227
+ def text_deltas(self) -> "Iterator[str]":
228
+ """Iterate only the visible text (no side channels, no events) — for plain-token consumers."""
229
+ if self._started:
230
+ raise InterfazeError("This stream has already been consumed.")
231
+ self._started = True
232
+ for chunk in self._client.chat.completions.create(stream=True, **self._kwargs):
233
+ self._state.accumulate(chunk)
234
+ if not chunk.choices:
235
+ continue
236
+ delta = chunk.choices[0].delta
237
+ piece = self._filter.feed(delta.content) if isinstance(delta.content, str) else ""
238
+ if chunk.choices[0].finish_reason:
239
+ piece += self._filter.flush()
240
+ if piece:
241
+ yield piece
242
+ self._done = True
243
+
244
+ @property
245
+ def text(self) -> str:
246
+ text = strip_side_channels(self._state.content)[0]
247
+ return strip_json_fence(text) if self._strip_fence else text
248
+
249
+ def get_final_completion(self) -> InterfazeChatCompletion:
250
+ if not self._started:
251
+ self._started = True
252
+ for chunk in self._client.chat.completions.create(stream=True, **self._kwargs):
253
+ self._state.accumulate(chunk)
254
+ self._done = True
255
+ elif not self._done:
256
+ raise InterfazeError(
257
+ "Call get_final_completion() after fully iterating, or instead of iterating."
258
+ )
259
+ return self._state.build(self._strip_fence)
260
+
261
+
262
+ class AsyncInterfazeStream:
263
+ """Async streaming helper — ``async for`` OpenAI-style events, then ``await get_final_completion()``."""
264
+
265
+ def __init__(self, client: AsyncOpenAI, kwargs: Dict[str, Any], strip_fence: bool = False) -> None:
266
+ self._client = client
267
+ self._kwargs = kwargs
268
+ self._strip_fence = strip_fence
269
+ self._state = _State()
270
+ self._openai_state: ChatCompletionStreamState[None] = ChatCompletionStreamState()
271
+ self._filter = _SideChannelFilter()
272
+ self._role_injected = False
273
+ self._started = False
274
+ self._done = False
275
+
276
+ async def __aenter__(self) -> "AsyncInterfazeStream":
277
+ return self
278
+
279
+ async def __aexit__(self, *exc: Any) -> None:
280
+ return None
281
+
282
+ async def __aiter__(self) -> "AsyncIterator[ChatCompletionStreamEvent[None]]":
283
+ if self._started:
284
+ raise InterfazeError("This stream has already been consumed.")
285
+ self._started = True
286
+ stream = await self._client.chat.completions.create(stream=True, **self._kwargs)
287
+ async for chunk in stream:
288
+ self._state.accumulate(chunk)
289
+ cleaned, self._role_injected = _clean_for_events(chunk, self._filter, self._role_injected)
290
+ for event in self._openai_state.handle_chunk(cleaned):
291
+ yield event
292
+ self._done = True
293
+
294
+ async def text_deltas(self) -> "AsyncIterator[str]":
295
+ """Iterate only the visible text (no side channels, no events) — for plain-token consumers."""
296
+ if self._started:
297
+ raise InterfazeError("This stream has already been consumed.")
298
+ self._started = True
299
+ stream = await self._client.chat.completions.create(stream=True, **self._kwargs)
300
+ async for chunk in stream:
301
+ self._state.accumulate(chunk)
302
+ if not chunk.choices:
303
+ continue
304
+ delta = chunk.choices[0].delta
305
+ piece = self._filter.feed(delta.content) if isinstance(delta.content, str) else ""
306
+ if chunk.choices[0].finish_reason:
307
+ piece += self._filter.flush()
308
+ if piece:
309
+ yield piece
310
+ self._done = True
311
+
312
+ async def get_final_completion(self) -> InterfazeChatCompletion:
313
+ if not self._started:
314
+ self._started = True
315
+ stream = await self._client.chat.completions.create(stream=True, **self._kwargs)
316
+ async for chunk in stream:
317
+ self._state.accumulate(chunk)
318
+ self._done = True
319
+ elif not self._done:
320
+ raise InterfazeError(
321
+ "Call get_final_completion() after fully iterating, or instead of iterating."
322
+ )
323
+ return self._state.build(self._strip_fence)
interfaze/_tasks.py ADDED
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+ from ._chat import AsyncCompletions, Completions
7
+ from ._inputs import auto_part
8
+ from ._types import TaskName
9
+
10
+ Content = Union[str, List[Dict[str, Any]]]
11
+
12
+
13
+ def _text(t: str) -> Dict[str, Any]:
14
+ return {"type": "text", "text": t}
15
+
16
+
17
+ def _extract(content: Optional[str]) -> Any:
18
+ if not content:
19
+ return None
20
+ try:
21
+ parsed = json.loads(content)
22
+ return parsed.get("result", parsed) if isinstance(parsed, dict) else parsed
23
+ except (ValueError, TypeError):
24
+ return content
25
+
26
+
27
+ def _forecast_prompt(csv_source: str, periods: int, unit: str) -> str:
28
+ return f"Forecast the next {periods} {unit} of this: {csv_source}"
29
+
30
+
31
+ class Tasks:
32
+ """High-level task helpers (sync). Each returns the task's raw ``result``."""
33
+
34
+ def __init__(self, completions: Completions) -> None:
35
+ self._c = completions
36
+
37
+ def _run(self, task: TaskName, content: Content) -> Any:
38
+ r = self._c.create(task=task, messages=[{"role": "user", "content": content}])
39
+ return _extract(r.choices[0].message.content)
40
+
41
+ def ocr(self, source: str, *, prompt: str = "Extract all text and data.") -> Any:
42
+ return self._run("ocr", [_text(prompt), auto_part(source)])
43
+
44
+ def object_detection(self, source: str, *, prompt: str = "Detect all objects.") -> Any:
45
+ return self._run("object_detection", [_text(prompt), auto_part(source)])
46
+
47
+ def gui_detection(self, source: str, *, prompt: str = "Detect all GUI elements.") -> Any:
48
+ return self._run("gui_detection", [_text(prompt), auto_part(source)])
49
+
50
+ def transcribe(self, source: str, *, prompt: str = "Transcribe this audio.") -> Any:
51
+ return self._run("speech_to_text", [_text(prompt), auto_part(source)])
52
+
53
+ def web_search(self, query: str) -> Any:
54
+ return self._run("web_search", query)
55
+
56
+ def scrape(self, url: str, *, prompt: str = "Scrape this page") -> Any:
57
+ return self._run("scraper", f"{prompt}: {url}")
58
+
59
+ def translate(self, text: str, *, to: str) -> Any:
60
+ return self._run("translate", f"Translate the following into {to}:\n\n{text}")
61
+
62
+ def forecast(self, csv_source: str, *, periods: int = 10, unit: str = "days") -> Any:
63
+ r = self._c.create(
64
+ messages=[{"role": "user", "content": _forecast_prompt(csv_source, periods, unit)}]
65
+ )
66
+ for p in r.precontext or []:
67
+ if p.name == "forecast":
68
+ return p.result
69
+ return r.choices[0].message.content
70
+
71
+
72
+ class AsyncTasks:
73
+ """High-level task helpers (async)."""
74
+
75
+ def __init__(self, completions: AsyncCompletions) -> None:
76
+ self._c = completions
77
+
78
+ async def _run(self, task: TaskName, content: Content) -> Any:
79
+ r = await self._c.create(task=task, messages=[{"role": "user", "content": content}])
80
+ return _extract(r.choices[0].message.content)
81
+
82
+ async def ocr(self, source: str, *, prompt: str = "Extract all text and data.") -> Any:
83
+ return await self._run("ocr", [_text(prompt), auto_part(source)])
84
+
85
+ async def object_detection(self, source: str, *, prompt: str = "Detect all objects.") -> Any:
86
+ return await self._run("object_detection", [_text(prompt), auto_part(source)])
87
+
88
+ async def gui_detection(self, source: str, *, prompt: str = "Detect all GUI elements.") -> Any:
89
+ return await self._run("gui_detection", [_text(prompt), auto_part(source)])
90
+
91
+ async def transcribe(self, source: str, *, prompt: str = "Transcribe this audio.") -> Any:
92
+ return await self._run("speech_to_text", [_text(prompt), auto_part(source)])
93
+
94
+ async def web_search(self, query: str) -> Any:
95
+ return await self._run("web_search", query)
96
+
97
+ async def scrape(self, url: str, *, prompt: str = "Scrape this page") -> Any:
98
+ return await self._run("scraper", f"{prompt}: {url}")
99
+
100
+ async def translate(self, text: str, *, to: str) -> Any:
101
+ return await self._run("translate", f"Translate the following into {to}:\n\n{text}")
102
+
103
+ async def forecast(self, csv_source: str, *, periods: int = 10, unit: str = "days") -> Any:
104
+ r = await self._c.create(
105
+ messages=[{"role": "user", "content": _forecast_prompt(csv_source, periods, unit)}]
106
+ )
107
+ for p in r.precontext or []:
108
+ if p.name == "forecast":
109
+ return p.result
110
+ return r.choices[0].message.content
interfaze/_types.py ADDED
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, List, Literal, Optional
4
+
5
+ from openai.types.chat import ChatCompletion
6
+ from pydantic import BaseModel, ConfigDict
7
+
8
+ TaskName = Literal[
9
+ "ocr",
10
+ "object_detection",
11
+ "gui_detection",
12
+ "web_search",
13
+ "scraper",
14
+ "translate",
15
+ "speech_to_text",
16
+ "forecast",
17
+ ]
18
+
19
+ GuardCode = Literal[
20
+ "S1",
21
+ "S2",
22
+ "S3",
23
+ "S4",
24
+ "S5",
25
+ "S6",
26
+ "S7",
27
+ "S8",
28
+ "S9",
29
+ "S10",
30
+ "S11",
31
+ "S12",
32
+ "S13",
33
+ "S14",
34
+ "S1_IMAGE",
35
+ "S12_IMAGE",
36
+ "S15_IMAGE",
37
+ "ALL",
38
+ ]
39
+
40
+ ReasoningEffort = Literal["minimal", "low", "medium", "high", "on", "off", "auto"]
41
+
42
+
43
+ class Precontext(BaseModel):
44
+ """One internal task's output; lenient since raw tool-call entries omit name/result."""
45
+
46
+ model_config = ConfigDict(extra="allow")
47
+ name: Optional[str] = None
48
+ result: Any = None
49
+
50
+
51
+ class InterfazeChatCompletion(ChatCompletion):
52
+ """ChatCompletion plus Interfaze extras: precontext, reasoning, vcache, debug."""
53
+
54
+ precontext: Optional[List[Precontext]] = None
55
+ reasoning: Optional[str] = None
56
+ vcache: bool = False
57
+ debug: Optional[Any] = None