bfocus 0.1.0__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.
- bfocus/__init__.py +45 -0
- bfocus/_client.py +95 -0
- bfocus/_resources.py +862 -0
- bfocus/_transport.py +335 -0
- bfocus/_version.py +3 -0
- bfocus/errors.py +131 -0
- bfocus/py.typed +0 -0
- bfocus/types.py +310 -0
- bfocus/widget.py +36 -0
- bfocus-0.1.0.dist-info/METADATA +384 -0
- bfocus-0.1.0.dist-info/RECORD +13 -0
- bfocus-0.1.0.dist-info/WHEEL +4 -0
- bfocus-0.1.0.dist-info/licenses/LICENSE +21 -0
bfocus/types.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Tipos da SDK do bFocus.
|
|
2
|
+
|
|
3
|
+
As respostas são ``dict`` comuns anotados com ``TypedDict``: campos que a API passar a
|
|
4
|
+
devolver no futuro continuam no dicionário (nunca viram erro). ``Page`` é o retorno das
|
|
5
|
+
listas paginadas.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Dict, Generic, Iterator, List, Literal, Optional, TypedDict, TypeVar, Union
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"UNSET",
|
|
13
|
+
"Unset",
|
|
14
|
+
"MaybeUnset",
|
|
15
|
+
"Page",
|
|
16
|
+
# entradas
|
|
17
|
+
"CustomFieldInput",
|
|
18
|
+
"KBBatchItem",
|
|
19
|
+
"AgentTurn",
|
|
20
|
+
# saídas
|
|
21
|
+
"CustomField",
|
|
22
|
+
"Customer",
|
|
23
|
+
"Contact",
|
|
24
|
+
"ProductRef",
|
|
25
|
+
"Product",
|
|
26
|
+
"Interaction",
|
|
27
|
+
"ReleaseNote",
|
|
28
|
+
"KBArticleSummary",
|
|
29
|
+
"KBArticle",
|
|
30
|
+
"KBSearchHit",
|
|
31
|
+
"KBBatchResult",
|
|
32
|
+
"KBBatchOutcome",
|
|
33
|
+
"AIAgent",
|
|
34
|
+
"AgentPreview",
|
|
35
|
+
"Deleted",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
T = TypeVar("T")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── "omitido" x "null" ───────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Unset:
|
|
45
|
+
"""Marca de argumento **não informado** (diferente de ``None``, que envia ``null``).
|
|
46
|
+
|
|
47
|
+
Os upserts são parciais: só o que você passa muda; ``None`` explícito LIMPA o campo.
|
|
48
|
+
Você nunca precisa usar esta classe — ela é o valor padrão dos kwargs opcionais.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
_instance: "Optional[Unset]" = None
|
|
52
|
+
|
|
53
|
+
def __new__(cls) -> "Unset":
|
|
54
|
+
if cls._instance is None:
|
|
55
|
+
cls._instance = super().__new__(cls)
|
|
56
|
+
return cls._instance
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
return "UNSET"
|
|
60
|
+
|
|
61
|
+
def __bool__(self) -> bool:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def __copy__(self) -> "Unset":
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __deepcopy__(self, memo: Any) -> "Unset":
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __reduce__(self) -> str:
|
|
71
|
+
return "UNSET"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
#: Valor padrão dos kwargs opcionais de corpo: "não envie este campo".
|
|
75
|
+
UNSET = Unset()
|
|
76
|
+
|
|
77
|
+
#: ``MaybeUnset[Optional[str]]`` = ``str`` | ``None`` (envia ``null``) | não informado.
|
|
78
|
+
MaybeUnset = Union[T, Unset]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── paginação ───────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Page(Generic[T]):
|
|
86
|
+
"""Uma página de resultados. Iterável sobre ``items``.
|
|
87
|
+
|
|
88
|
+
Para percorrer tudo sem controlar páginas, use o ``list_all(...)`` do recurso.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
items: List[T]
|
|
92
|
+
page: int
|
|
93
|
+
page_size: int
|
|
94
|
+
total: int
|
|
95
|
+
pages: int
|
|
96
|
+
|
|
97
|
+
def __iter__(self) -> Iterator[T]:
|
|
98
|
+
return iter(self.items)
|
|
99
|
+
|
|
100
|
+
def __len__(self) -> int:
|
|
101
|
+
return len(self.items)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def has_next(self) -> bool:
|
|
105
|
+
"""``True`` se existe página depois desta."""
|
|
106
|
+
return self.page < self.pages
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── entradas ────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _CustomFieldInputRequired(TypedDict):
|
|
113
|
+
key: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class CustomFieldInput(_CustomFieldInputRequired, total=False):
|
|
117
|
+
"""Campo personalizado de cliente. Enviar a lista SUBSTITUI a lista inteira."""
|
|
118
|
+
|
|
119
|
+
label: str
|
|
120
|
+
type: Literal[
|
|
121
|
+
"text", "textarea", "email", "phone", "url", "number", "date", "datetime",
|
|
122
|
+
"bool", "select", "file",
|
|
123
|
+
]
|
|
124
|
+
value: Any
|
|
125
|
+
options: Optional[List[str]]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class _KBBatchItemRequired(TypedDict):
|
|
129
|
+
external_id: str
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class KBBatchItem(_KBBatchItemRequired, total=False):
|
|
133
|
+
"""Item do ``kb.articles.batch_upsert``. ``product: None`` torna o artigo global."""
|
|
134
|
+
|
|
135
|
+
title: Optional[str]
|
|
136
|
+
body_html: Optional[str]
|
|
137
|
+
body_markdown: Optional[str]
|
|
138
|
+
product: Optional[str]
|
|
139
|
+
status: Optional[Literal["draft", "published"]]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class AgentTurn(TypedDict):
|
|
143
|
+
"""Turno do histórico de ``ai_agents.preview``."""
|
|
144
|
+
|
|
145
|
+
role: Literal["customer", "bot"]
|
|
146
|
+
content: str
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── saídas ──────────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class CustomField(TypedDict):
|
|
153
|
+
key: str
|
|
154
|
+
label: Optional[str]
|
|
155
|
+
type: str
|
|
156
|
+
value: Any
|
|
157
|
+
visibility: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Customer(TypedDict):
|
|
161
|
+
id: str
|
|
162
|
+
external_id: str
|
|
163
|
+
name: str
|
|
164
|
+
document: Optional[str]
|
|
165
|
+
email: Optional[str]
|
|
166
|
+
phone: Optional[str]
|
|
167
|
+
website: Optional[str]
|
|
168
|
+
notes: Optional[str]
|
|
169
|
+
custom_fields: List[CustomField]
|
|
170
|
+
is_active: bool
|
|
171
|
+
created_at: Optional[str]
|
|
172
|
+
updated_at: Optional[str]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Contact(TypedDict):
|
|
176
|
+
id: str
|
|
177
|
+
external_id: Optional[str]
|
|
178
|
+
name: str
|
|
179
|
+
role: Optional[str]
|
|
180
|
+
email: Optional[str]
|
|
181
|
+
phone: Optional[str]
|
|
182
|
+
notes: Optional[str]
|
|
183
|
+
is_primary: bool
|
|
184
|
+
created_at: Optional[str]
|
|
185
|
+
updated_at: Optional[str]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ProductRef(TypedDict):
|
|
189
|
+
id: str
|
|
190
|
+
slug: str
|
|
191
|
+
name: str
|
|
192
|
+
is_active: bool
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class Product(TypedDict):
|
|
196
|
+
id: str
|
|
197
|
+
slug: str
|
|
198
|
+
name: str
|
|
199
|
+
description: Optional[str]
|
|
200
|
+
color: Optional[str]
|
|
201
|
+
icon: Optional[str]
|
|
202
|
+
is_active: bool
|
|
203
|
+
sort_order: int
|
|
204
|
+
current_version: str
|
|
205
|
+
ai_level: str
|
|
206
|
+
created_at: Optional[str]
|
|
207
|
+
updated_at: Optional[str]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class Interaction(TypedDict):
|
|
211
|
+
id: str
|
|
212
|
+
content: str
|
|
213
|
+
is_internal: bool
|
|
214
|
+
author_kind: str
|
|
215
|
+
author_name: Optional[str]
|
|
216
|
+
created_at: Optional[str]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class ReleaseNote(TypedDict):
|
|
220
|
+
id: str
|
|
221
|
+
product: str
|
|
222
|
+
version: str
|
|
223
|
+
title: str
|
|
224
|
+
description_html: str
|
|
225
|
+
audience: str
|
|
226
|
+
is_published: bool
|
|
227
|
+
require_ack_internal: bool
|
|
228
|
+
require_ack_external: bool
|
|
229
|
+
published_at: Optional[str]
|
|
230
|
+
created_at: Optional[str]
|
|
231
|
+
updated_at: Optional[str]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class KBArticleSummary(TypedDict):
|
|
235
|
+
"""Artigo sem o corpo — itens de ``kb.articles.list``/``list_all`` e do ``batch_upsert``."""
|
|
236
|
+
|
|
237
|
+
id: str
|
|
238
|
+
external_id: Optional[str]
|
|
239
|
+
product: Optional[str]
|
|
240
|
+
title: str
|
|
241
|
+
excerpt: str
|
|
242
|
+
status: str
|
|
243
|
+
origin: str
|
|
244
|
+
published_at: Optional[str]
|
|
245
|
+
created_at: Optional[str]
|
|
246
|
+
updated_at: Optional[str]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class KBArticle(KBArticleSummary):
|
|
250
|
+
"""Artigo completo (``get``/``upsert``/``publish``/``unpublish``): resumo + ``body_html``."""
|
|
251
|
+
|
|
252
|
+
body_html: Optional[str]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class KBSearchHit(TypedDict):
|
|
256
|
+
id: str
|
|
257
|
+
external_id: Optional[str]
|
|
258
|
+
title: str
|
|
259
|
+
excerpt: str
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class KBBatchResult(TypedDict):
|
|
263
|
+
external_id: str
|
|
264
|
+
ok: bool
|
|
265
|
+
action: Optional[Literal["created", "updated", "unchanged"]]
|
|
266
|
+
error: Optional[str]
|
|
267
|
+
article: Optional[KBArticleSummary]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class KBBatchOutcome(TypedDict):
|
|
271
|
+
"""Resultado agregado de ``batch_upsert`` (todos os lotes, na ordem enviada)."""
|
|
272
|
+
|
|
273
|
+
results: List[KBBatchResult]
|
|
274
|
+
created: int
|
|
275
|
+
updated: int
|
|
276
|
+
unchanged: int
|
|
277
|
+
failed: int
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class AIAgent(TypedDict):
|
|
281
|
+
id: str
|
|
282
|
+
name: str
|
|
283
|
+
product: ProductRef
|
|
284
|
+
active: bool
|
|
285
|
+
persona: Optional[str]
|
|
286
|
+
scope: Optional[str]
|
|
287
|
+
avatar_url: Optional[str]
|
|
288
|
+
created_at: Optional[str]
|
|
289
|
+
updated_at: Optional[str]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class AgentPreview(TypedDict):
|
|
293
|
+
"""Resposta simulada do agente. ``action``: ``answer``, ``handoff`` ou ``refuse``."""
|
|
294
|
+
|
|
295
|
+
action: str
|
|
296
|
+
answer_html: Optional[str]
|
|
297
|
+
escalated: bool
|
|
298
|
+
refused: bool
|
|
299
|
+
handoff_reason: Optional[str]
|
|
300
|
+
confidence: Optional[float]
|
|
301
|
+
topic: Optional[str]
|
|
302
|
+
guards: List[str]
|
|
303
|
+
citations: List[Any]
|
|
304
|
+
sources: List[Dict[str, Any]]
|
|
305
|
+
collected: Dict[str, Any]
|
|
306
|
+
missing: List[Any]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class Deleted(TypedDict):
|
|
310
|
+
deleted: bool
|
bfocus/widget.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Identidade do widget de atendimento (assinatura local, sem rede)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
__all__ = ["sign_widget_identity"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def sign_widget_identity(
|
|
13
|
+
secret: Union[str, bytes],
|
|
14
|
+
user_external_id: str,
|
|
15
|
+
customer_external_id: str,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Assina a identidade do usuário logado para abrir o widget do bFocus.
|
|
18
|
+
|
|
19
|
+
Roda no **seu backend** (o segredo nunca vai para o navegador) e não exige chave de API.
|
|
20
|
+
Devolve o HMAC-SHA256 em hexadecimal minúsculo de
|
|
21
|
+
``"v1:" + user_external_id + ":" + customer_external_id`` (UTF-8).
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
secret: Segredo de identidade do widget (painel do bFocus).
|
|
25
|
+
user_external_id: ``external_id`` do usuário no seu sistema.
|
|
26
|
+
customer_external_id: ``external_id`` do cliente (empresa) desse usuário.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> sign_widget_identity("bf_whs_x", "USR-1", "ACME-1")[:16]
|
|
30
|
+
'9a15d2527b855a04'
|
|
31
|
+
"""
|
|
32
|
+
key = secret.encode("utf-8") if isinstance(secret, str) else bytes(secret)
|
|
33
|
+
if not key:
|
|
34
|
+
raise ValueError("sign_widget_identity: secret é obrigatório.")
|
|
35
|
+
msg = f"v1:{user_external_id}:{customer_external_id}".encode("utf-8")
|
|
36
|
+
return hmac.new(key, msg, hashlib.sha256).hexdigest()
|