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/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()