atendentepro 0.6.4__py3-none-any.whl → 0.6.6__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.
atendentepro/__init__.py CHANGED
@@ -158,6 +158,13 @@ from atendentepro.utils import (
158
158
  run_with_monkai_tracking,
159
159
  # Application Insights
160
160
  configure_application_insights,
161
+ # User Loader
162
+ create_user_loader,
163
+ run_with_user_context,
164
+ extract_phone_from_messages,
165
+ extract_email_from_messages,
166
+ extract_user_id_from_messages,
167
+ load_user_from_csv,
161
168
  )
162
169
 
163
170
  __all__ = [
@@ -253,5 +260,12 @@ __all__ = [
253
260
  "run_with_monkai_tracking",
254
261
  # Application Insights
255
262
  "configure_application_insights",
263
+ # User Loader
264
+ "create_user_loader",
265
+ "run_with_user_context",
266
+ "extract_phone_from_messages",
267
+ "extract_email_from_messages",
268
+ "extract_user_id_from_messages",
269
+ "load_user_from_csv",
256
270
  ]
257
271
 
atendentepro/network.py CHANGED
@@ -157,6 +157,10 @@ class AgentNetwork:
157
157
  templates_root: Optional[Path] = None
158
158
  current_client: str = "standard"
159
159
 
160
+ # User loading
161
+ user_loader: Optional[Callable[[List], Optional["UserContext"]]] = None
162
+ loaded_user_context: Optional["UserContext"] = None
163
+
160
164
  def get_all_agents(self) -> List:
161
165
  """Get list of all configured agents."""
162
166
  agents = []
@@ -216,6 +220,9 @@ def create_standard_network(
216
220
  agent_filters: Optional[Dict[str, AccessFilter]] = None,
217
221
  conditional_prompts: Optional[Dict[str, List[FilteredPromptSection]]] = None,
218
222
  filtered_tools: Optional[Dict[str, List[FilteredTool]]] = None,
223
+ # User loading
224
+ user_loader: Optional[Callable[[List], Optional[UserContext]]] = None,
225
+ auto_load_user: bool = False,
219
226
  ) -> AgentNetwork:
220
227
  """
221
228
  Create a standard agent network with proper handoff configuration.
@@ -259,6 +266,10 @@ def create_standard_network(
259
266
  agent_filters: Dict mapping agent names to AccessFilter (controls agent access).
260
267
  conditional_prompts: Dict mapping agent names to list of FilteredPromptSection.
261
268
  filtered_tools: Dict mapping agent names to list of FilteredTool.
269
+ user_loader: Optional function to load user data from messages. Receives list of
270
+ messages and returns UserContext or None. See create_user_loader().
271
+ auto_load_user: If True, automatically loads user context before agent execution.
272
+ Requires user_loader to be configured.
262
273
 
263
274
  Returns:
264
275
  Configured AgentNetwork instance.
@@ -686,6 +697,9 @@ def create_standard_network(
686
697
  network.onboarding = onboarding
687
698
  onboarding.handoffs = [triage]
688
699
 
700
+ # Store user_loader in network for later use
701
+ network.user_loader = user_loader
702
+
689
703
  return network
690
704
 
691
705
 
@@ -19,6 +19,14 @@ from .tracing import (
19
19
  # Legacy
20
20
  configure_tracing,
21
21
  )
22
+ from .user_loader import (
23
+ create_user_loader,
24
+ run_with_user_context,
25
+ extract_phone_from_messages,
26
+ extract_email_from_messages,
27
+ extract_user_id_from_messages,
28
+ load_user_from_csv,
29
+ )
22
30
 
23
31
  __all__ = [
24
32
  # OpenAI Client
@@ -36,5 +44,12 @@ __all__ = [
36
44
  "configure_application_insights",
37
45
  # Legacy
38
46
  "configure_tracing",
47
+ # User Loader
48
+ "create_user_loader",
49
+ "run_with_user_context",
50
+ "extract_phone_from_messages",
51
+ "extract_email_from_messages",
52
+ "extract_user_id_from_messages",
53
+ "load_user_from_csv",
39
54
  ]
40
55
 
@@ -0,0 +1,335 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ User Loader utilities for AtendentePro.
4
+
5
+ Provides functions to load user data from various sources and create UserContext
6
+ objects for enriching agent conversations with user information.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import csv
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Dict, List, Optional
15
+
16
+ from atendentepro.models import UserContext
17
+
18
+
19
+ def extract_phone_from_messages(messages: List[Dict[str, Any]]) -> Optional[str]:
20
+ """
21
+ Extract phone number from user messages.
22
+
23
+ Searches for phone patterns in the first user message.
24
+ Supports formats: (11) 99999-9999, 11999999999, +55 11 99999-9999, etc.
25
+
26
+ Args:
27
+ messages: List of message dictionaries with 'role' and 'content' keys.
28
+
29
+ Returns:
30
+ Phone number string if found, None otherwise.
31
+
32
+ Example:
33
+ >>> messages = [{"role": "user", "content": "Meu telefone é (11) 99999-8888"}]
34
+ >>> extract_phone_from_messages(messages)
35
+ '11999998888'
36
+ """
37
+ if not messages:
38
+ return None
39
+
40
+ # Find first user message
41
+ user_message = None
42
+ for msg in messages:
43
+ if msg.get("role") == "user" and msg.get("content"):
44
+ user_message = msg["content"]
45
+ break
46
+
47
+ if not user_message:
48
+ return None
49
+
50
+ # Phone patterns: (XX) XXXXX-XXXX, XX XXXXXXXX, +55 XX XXXXXXXX, etc.
51
+ phone_patterns = [
52
+ r'\(?(\d{2})\)?\s*(\d{4,5})-?(\d{4})', # (11) 99999-8888 or 11 99999-8888
53
+ r'\+?55\s*(\d{2})\s*(\d{4,5})-?(\d{4})', # +55 11 99999-8888
54
+ r'(\d{10,11})', # 11999998888
55
+ ]
56
+
57
+ for pattern in phone_patterns:
58
+ match = re.search(pattern, user_message)
59
+ if match:
60
+ # Extract digits only
61
+ digits = re.sub(r'\D', '', match.group(0))
62
+ # Normalize to 10 or 11 digits (with or without area code)
63
+ if len(digits) >= 10:
64
+ return digits[-10:] if len(digits) > 10 else digits
65
+
66
+ return None
67
+
68
+
69
+ def extract_email_from_messages(messages: List[Dict[str, Any]]) -> Optional[str]:
70
+ """
71
+ Extract email address from user messages.
72
+
73
+ Searches for email patterns in user messages.
74
+
75
+ Args:
76
+ messages: List of message dictionaries with 'role' and 'content' keys.
77
+
78
+ Returns:
79
+ Email address string if found, None otherwise.
80
+
81
+ Example:
82
+ >>> messages = [{"role": "user", "content": "Meu email é joao@example.com"}]
83
+ >>> extract_email_from_messages(messages)
84
+ 'joao@example.com'
85
+ """
86
+ if not messages:
87
+ return None
88
+
89
+ # Find first user message
90
+ user_message = None
91
+ for msg in messages:
92
+ if msg.get("role") == "user" and msg.get("content"):
93
+ user_message = msg["content"]
94
+ break
95
+
96
+ if not user_message:
97
+ return None
98
+
99
+ # Email pattern
100
+ email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
101
+ match = re.search(email_pattern, user_message)
102
+
103
+ if match:
104
+ return match.group(0).lower()
105
+
106
+ return None
107
+
108
+
109
+ def extract_user_id_from_messages(messages: List[Dict[str, Any]]) -> Optional[str]:
110
+ """
111
+ Extract user ID from user messages.
112
+
113
+ Looks for common patterns like "user_id:", "id:", "CPF:", etc.
114
+
115
+ Args:
116
+ messages: List of message dictionaries with 'role' and 'content' keys.
117
+
118
+ Returns:
119
+ User ID string if found, None otherwise.
120
+
121
+ Example:
122
+ >>> messages = [{"role": "user", "content": "Meu CPF é 123.456.789-00"}]
123
+ >>> extract_user_id_from_messages(messages)
124
+ '12345678900'
125
+ """
126
+ if not messages:
127
+ return None
128
+
129
+ # Find first user message
130
+ user_message = None
131
+ for msg in messages:
132
+ if msg.get("role") == "user" and msg.get("content"):
133
+ user_message = msg["content"]
134
+ break
135
+
136
+ if not user_message:
137
+ return None
138
+
139
+ # Patterns for user IDs
140
+ patterns = [
141
+ (r'(?:user[_\s]?id|id[_\s]?usuario|cpf|documento)[:\s]+([\d\.\-\/]+)', lambda m: re.sub(r'\D', '', m.group(1))),
142
+ (r'([\d]{11})', lambda m: m.group(1)), # CPF-like (11 digits)
143
+ (r'([\d]{14})', lambda m: m.group(1)), # CNPJ-like (14 digits)
144
+ ]
145
+
146
+ for pattern, extractor in patterns:
147
+ match = re.search(pattern, user_message, re.IGNORECASE)
148
+ if match:
149
+ return extractor(match)
150
+
151
+ return None
152
+
153
+
154
+ def load_user_from_csv(
155
+ csv_path: Path,
156
+ identifier_field: str,
157
+ identifier_value: str,
158
+ ) -> Optional[Dict[str, Any]]:
159
+ """
160
+ Load user data from a CSV file.
161
+
162
+ Args:
163
+ csv_path: Path to the CSV file.
164
+ identifier_field: Name of the column to search (e.g., "email", "telefone", "cpf").
165
+ identifier_value: Value to search for in the identifier_field column.
166
+
167
+ Returns:
168
+ Dictionary with user data if found, None otherwise.
169
+
170
+ Example:
171
+ >>> from pathlib import Path
172
+ >>> user_data = load_user_from_csv(
173
+ ... Path("users.csv"),
174
+ ... "email",
175
+ ... "joao@example.com"
176
+ ... )
177
+ >>> if user_data:
178
+ ... print(user_data["nome"])
179
+ """
180
+ if not csv_path.exists():
181
+ return None
182
+
183
+ try:
184
+ with open(csv_path, 'r', encoding='utf-8') as f:
185
+ reader = csv.DictReader(f)
186
+
187
+ # Normalize identifier value (remove formatting)
188
+ normalized_value = re.sub(r'\D', '', str(identifier_value).lower())
189
+
190
+ for row in reader:
191
+ # Get value from identifier field
192
+ field_value = row.get(identifier_field, '')
193
+ # Normalize for comparison
194
+ normalized_field = re.sub(r'\D', '', str(field_value).lower())
195
+
196
+ if normalized_field == normalized_value:
197
+ # Return all row data as dict
198
+ return dict(row)
199
+
200
+ except Exception:
201
+ return None
202
+
203
+ return None
204
+
205
+
206
+ def create_user_loader(
207
+ loader_func: Callable[[str], Optional[Dict[str, Any]]],
208
+ identifier_extractor: Optional[Callable[[List[Dict[str, Any]]], Optional[str]]] = None,
209
+ ) -> Callable[[List[Dict[str, Any]]], Optional[UserContext]]:
210
+ """
211
+ Create a user loader function.
212
+
213
+ Factory function that creates a callable that extracts user identifier from
214
+ messages and loads user data using the provided loader function.
215
+
216
+ Args:
217
+ loader_func: Function that receives an identifier (str) and returns
218
+ user data dict or None if not found.
219
+ identifier_extractor: Optional function to extract identifier from messages.
220
+ If None, tries common extractors (phone, email, user_id).
221
+
222
+ Returns:
223
+ Function that receives messages and returns UserContext or None.
224
+
225
+ Example:
226
+ >>> def load_from_db(identifier: str) -> Optional[Dict]:
227
+ ... # Your database lookup logic
228
+ ... return {"user_id": "123", "role": "cliente", "nome": "João"}
229
+ ...
230
+ >>> loader = create_user_loader(
231
+ ... loader_func=load_from_db,
232
+ ... identifier_extractor=extract_email_from_messages
233
+ ... )
234
+ >>>
235
+ >>> messages = [{"role": "user", "content": "joao@example.com"}]
236
+ >>> user_context = loader(messages)
237
+ >>> if user_context:
238
+ ... print(user_context.user_id)
239
+ """
240
+ def load_user(messages: List[Dict[str, Any]]) -> Optional[UserContext]:
241
+ """
242
+ Load user context from messages.
243
+
244
+ Args:
245
+ messages: List of message dictionaries.
246
+
247
+ Returns:
248
+ UserContext if user found, None otherwise.
249
+ """
250
+ # Extract identifier
251
+ identifier = None
252
+ if identifier_extractor:
253
+ identifier = identifier_extractor(messages)
254
+ else:
255
+ # Try common extractors in order
256
+ identifier = (
257
+ extract_phone_from_messages(messages) or
258
+ extract_email_from_messages(messages) or
259
+ extract_user_id_from_messages(messages)
260
+ )
261
+
262
+ if not identifier:
263
+ return None
264
+
265
+ # Load user data
266
+ user_data = loader_func(identifier)
267
+ if not user_data:
268
+ return None
269
+
270
+ # Create UserContext
271
+ return UserContext(
272
+ user_id=user_data.get("user_id") or identifier,
273
+ role=user_data.get("role"),
274
+ metadata={
275
+ **user_data, # Include all user data in metadata
276
+ "loaded_from": "user_loader",
277
+ }
278
+ )
279
+
280
+ return load_user
281
+
282
+
283
+ async def run_with_user_context(
284
+ network: Any,
285
+ agent: Any,
286
+ messages: List[Dict[str, Any]],
287
+ ) -> Any:
288
+ """
289
+ Run agent with automatic user context loading.
290
+
291
+ This function automatically loads user data if a user_loader is configured
292
+ in the network, enriches the network's loaded_user_context, and then runs
293
+ the agent normally.
294
+
295
+ Args:
296
+ network: AgentNetwork instance with optional user_loader configured.
297
+ agent: Agent instance to run.
298
+ messages: List of message dictionaries.
299
+
300
+ Returns:
301
+ RunResult from the agent execution.
302
+
303
+ Example:
304
+ >>> from agents import Runner
305
+ >>> from atendentepro import create_standard_network, create_user_loader
306
+ >>> from pathlib import Path
307
+ >>>
308
+ >>> def load_user(identifier: str):
309
+ ... # Your loading logic
310
+ ... return {"user_id": identifier, "role": "cliente"}
311
+ >>>
312
+ >>> loader = create_user_loader(load_user)
313
+ >>> network = create_standard_network(
314
+ ... templates_root=Path("templates"),
315
+ ... user_loader=loader
316
+ ... )
317
+ >>>
318
+ >>> messages = [{"role": "user", "content": "Olá!"}]
319
+ >>> result = await run_with_user_context(network, network.triage, messages)
320
+ >>> print(result.final_output)
321
+ """
322
+ from agents import Runner
323
+
324
+ # Load user if loader is configured
325
+ if hasattr(network, 'user_loader') and network.user_loader:
326
+ try:
327
+ user_context = network.user_loader(messages)
328
+ if user_context:
329
+ network.loaded_user_context = user_context
330
+ except Exception:
331
+ # Silently fail if loading fails - don't break the conversation
332
+ pass
333
+
334
+ # Run agent normally
335
+ return await Runner.run(agent, messages)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atendentepro
3
- Version: 0.6.4
3
+ Version: 0.6.6
4
4
  Summary: Framework de orquestração de agentes IA com tom e estilo customizáveis. Integra documentos (RAG), APIs e bancos de dados em uma plataforma inteligente multi-agente.
5
5
  Author-email: BeMonkAI <contato@monkai.com.br>
6
6
  Maintainer-email: BeMonkAI <contato@monkai.com.br>
@@ -96,6 +96,8 @@ Plataforma que unifica múltiplos agentes especializados para resolver demandas
96
96
  - [Estilo de Comunicação](#-estilo-de-comunicação-agentstyle)
97
97
  - [Single Reply Mode](#-single-reply-mode)
98
98
  - [Filtros de Acesso](#-filtros-de-acesso-roleuser)
99
+ - [Carregamento de Usuários](#-carregamento-de-usuários-user-loader)
100
+ - [Múltiplos Agentes](#-múltiplos-agentes-multi-interview--knowledge)
99
101
  - [Tracing e Monitoramento](#-tracing-e-monitoramento)
100
102
  - [Suporte](#-suporte)
101
103
 
@@ -1039,6 +1041,317 @@ tool_access:
1039
1041
 
1040
1042
  ---
1041
1043
 
1044
+ ## 👤 Carregamento de Usuários (User Loader)
1045
+
1046
+ O **User Loader** identifica automaticamente usuários cadastrados nas conversas e carrega suas informações para enriquecer o contexto, permitindo personalização e evitando onboarding desnecessário.
1047
+
1048
+ 📂 **Exemplos completos**: [docs/examples/user_loader/](docs/examples/user_loader/)
1049
+
1050
+ ### Quando Usar
1051
+
1052
+ | Cenário | Solução |
1053
+ |---------|---------|
1054
+ | **Usuário existente** | Identifica automaticamente e pula onboarding |
1055
+ | **Personalização** | Carrega dados do usuário para respostas personalizadas |
1056
+ | **Contexto enriquecido** | Todos os agentes têm acesso a informações do usuário |
1057
+ | **Múltiplas fontes** | Suporta CSV, banco de dados, APIs REST, etc. |
1058
+
1059
+ ### Funcionalidades
1060
+
1061
+ 1. **Extração automática** de identificadores (telefone, email, CPF, etc.)
1062
+ 2. **Carregamento de dados** de múltiplas fontes
1063
+ 3. **Criação automática** de `UserContext`
1064
+ 4. **Integração transparente** com a rede de agentes
1065
+
1066
+ ### Exemplo 1: Carregamento de CSV
1067
+
1068
+ ```python
1069
+ from pathlib import Path
1070
+ from atendentepro import (
1071
+ create_standard_network,
1072
+ create_user_loader,
1073
+ load_user_from_csv,
1074
+ extract_email_from_messages,
1075
+ run_with_user_context,
1076
+ )
1077
+
1078
+ # Função para carregar do CSV
1079
+ def load_user(identifier: str):
1080
+ return load_user_from_csv(
1081
+ csv_path=Path("users.csv"),
1082
+ identifier_field="email",
1083
+ identifier_value=identifier
1084
+ )
1085
+
1086
+ # Criar loader
1087
+ loader = create_user_loader(
1088
+ loader_func=load_user,
1089
+ identifier_extractor=extract_email_from_messages
1090
+ )
1091
+
1092
+ # Criar network com loader
1093
+ network = create_standard_network(
1094
+ templates_root=Path("./templates"),
1095
+ user_loader=loader,
1096
+ include_onboarding=True,
1097
+ )
1098
+
1099
+ # Executar com carregamento automático
1100
+ messages = [{"role": "user", "content": "Meu email é joao@example.com"}]
1101
+ result = await run_with_user_context(network, network.triage, messages)
1102
+
1103
+ # Verificar se usuário foi carregado
1104
+ if network.loaded_user_context:
1105
+ print(f"Usuário: {network.loaded_user_context.metadata.get('nome')}")
1106
+ ```
1107
+
1108
+ ### Exemplo 2: Carregamento de Banco de Dados
1109
+
1110
+ ```python
1111
+ import sqlite3
1112
+ from atendentepro import create_user_loader, extract_email_from_messages
1113
+
1114
+ def load_from_db(identifier: str):
1115
+ conn = sqlite3.connect("users.db")
1116
+ cursor = conn.cursor()
1117
+ cursor.execute("SELECT * FROM users WHERE email = ?", (identifier,))
1118
+ row = cursor.fetchone()
1119
+ conn.close()
1120
+
1121
+ if row:
1122
+ return {
1123
+ "user_id": row[0],
1124
+ "role": row[1],
1125
+ "nome": row[2],
1126
+ "email": row[3],
1127
+ }
1128
+ return None
1129
+
1130
+ loader = create_user_loader(load_from_db, extract_email_from_messages)
1131
+
1132
+ network = create_standard_network(
1133
+ templates_root=Path("./templates"),
1134
+ user_loader=loader,
1135
+ )
1136
+ ```
1137
+
1138
+ ### Exemplo 3: Múltiplos Identificadores
1139
+
1140
+ ```python
1141
+ from atendentepro import (
1142
+ create_user_loader,
1143
+ extract_email_from_messages,
1144
+ extract_phone_from_messages,
1145
+ )
1146
+
1147
+ def extract_identifier(messages):
1148
+ # Tenta email primeiro
1149
+ email = extract_email_from_messages(messages)
1150
+ if email:
1151
+ return email
1152
+
1153
+ # Se não encontrou, tenta telefone
1154
+ phone = extract_phone_from_messages(messages)
1155
+ if phone:
1156
+ return phone
1157
+
1158
+ return None
1159
+
1160
+ loader = create_user_loader(
1161
+ loader_func=load_user,
1162
+ identifier_extractor=extract_identifier
1163
+ )
1164
+ ```
1165
+
1166
+ ### Funções Disponíveis
1167
+
1168
+ #### Extratores de Identificador
1169
+
1170
+ ```python
1171
+ from atendentepro import (
1172
+ extract_phone_from_messages, # Extrai telefone
1173
+ extract_email_from_messages, # Extrai email
1174
+ extract_user_id_from_messages, # Extrai CPF/user_id
1175
+ )
1176
+ ```
1177
+
1178
+ #### Criar Loader
1179
+
1180
+ ```python
1181
+ from atendentepro import create_user_loader
1182
+
1183
+ loader = create_user_loader(
1184
+ loader_func=load_user_function,
1185
+ identifier_extractor=extract_email_from_messages # Opcional
1186
+ )
1187
+ ```
1188
+
1189
+ #### Executar com Contexto
1190
+
1191
+ ```python
1192
+ from atendentepro import run_with_user_context
1193
+
1194
+ result = await run_with_user_context(
1195
+ network,
1196
+ network.triage,
1197
+ messages
1198
+ )
1199
+ ```
1200
+
1201
+ ### Integração com Onboarding
1202
+
1203
+ Quando um `user_loader` está configurado:
1204
+
1205
+ - ✅ **Usuário encontrado**: Vai direto para o triage, sem passar pelo onboarding
1206
+ - ✅ **Usuário não encontrado**: É direcionado para o onboarding normalmente
1207
+ - ✅ **Contexto disponível**: Todos os agentes têm acesso a `network.loaded_user_context`
1208
+
1209
+ ### Benefícios
1210
+
1211
+ 1. ✅ **Experiência personalizada** - Respostas baseadas em dados do usuário
1212
+ 2. ✅ **Menos fricção** - Usuários conhecidos não precisam fazer onboarding
1213
+ 3. ✅ **Contexto rico** - Todos os agentes têm acesso a informações do usuário
1214
+ 4. ✅ **Flexível** - Suporta múltiplas fontes de dados
1215
+ 5. ✅ **Automático** - Funciona transparentemente durante a conversa
1216
+
1217
+ ---
1218
+
1219
+ ## 🔀 Múltiplos Agentes (Multi Interview + Knowledge)
1220
+
1221
+ O AtendentePro suporta criar **múltiplas instâncias** de Interview e Knowledge agents, cada um especializado em um domínio diferente.
1222
+
1223
+ 📂 **Exemplo completo**: [docs/examples/multi_agents/](docs/examples/multi_agents/)
1224
+
1225
+ ### Caso de Uso
1226
+
1227
+ Empresa que atende diferentes tipos de clientes:
1228
+ - **Pessoa Física (PF)**: Produtos de consumo
1229
+ - **Pessoa Jurídica (PJ)**: Soluções empresariais
1230
+
1231
+ ### Arquitetura
1232
+
1233
+ ```
1234
+ ┌─────────────────┐
1235
+ │ Triage │
1236
+ │ (entry point) │
1237
+ └────────┬────────┘
1238
+
1239
+ ┌──────────────┼──────────────┐
1240
+ │ │ │
1241
+ ▼ ▼ ▼
1242
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
1243
+ │ Interview │ │ Interview │ │ Flow │
1244
+ │ PF │ │ PJ │ │ (comum) │
1245
+ └──────┬──────┘ └──────┬──────┘ └─────────────┘
1246
+ │ │
1247
+ ▼ ▼
1248
+ ┌─────────────┐ ┌─────────────┐
1249
+ │ Knowledge │ │ Knowledge │
1250
+ │ PF │ │ PJ │
1251
+ └─────────────┘ └─────────────┘
1252
+ ```
1253
+
1254
+ ### Implementação
1255
+
1256
+ ```python
1257
+ from atendentepro import (
1258
+ create_custom_network,
1259
+ create_triage_agent,
1260
+ create_interview_agent,
1261
+ create_knowledge_agent,
1262
+ )
1263
+
1264
+ # 1. Criar agentes especializados
1265
+ interview_pf = create_interview_agent(
1266
+ interview_questions="CPF, data de nascimento, renda mensal",
1267
+ name="interview_pf", # Nome único!
1268
+ )
1269
+
1270
+ interview_pj = create_interview_agent(
1271
+ interview_questions="CNPJ, razão social, faturamento",
1272
+ name="interview_pj", # Nome único!
1273
+ )
1274
+
1275
+ knowledge_pf = create_knowledge_agent(
1276
+ knowledge_about="Produtos para consumidor final",
1277
+ name="knowledge_pf",
1278
+ single_reply=True,
1279
+ )
1280
+
1281
+ knowledge_pj = create_knowledge_agent(
1282
+ knowledge_about="Soluções empresariais B2B",
1283
+ name="knowledge_pj",
1284
+ single_reply=True,
1285
+ )
1286
+
1287
+ # 2. Criar Triage
1288
+ triage = create_triage_agent(
1289
+ keywords_text="PF: CPF, pessoal, minha conta | PJ: CNPJ, empresa, MEI",
1290
+ name="triage_agent",
1291
+ )
1292
+
1293
+ # 3. Configurar handoffs
1294
+ triage.handoffs = [interview_pf, interview_pj, knowledge_pf, knowledge_pj]
1295
+ interview_pf.handoffs = [knowledge_pf, triage]
1296
+ interview_pj.handoffs = [knowledge_pj, triage]
1297
+ knowledge_pf.handoffs = [triage]
1298
+ knowledge_pj.handoffs = [triage]
1299
+
1300
+ # 4. Criar network customizada
1301
+ network = create_custom_network(
1302
+ triage=triage,
1303
+ custom_agents={
1304
+ "interview_pf": interview_pf,
1305
+ "interview_pj": interview_pj,
1306
+ "knowledge_pf": knowledge_pf,
1307
+ "knowledge_pj": knowledge_pj,
1308
+ },
1309
+ )
1310
+ ```
1311
+
1312
+ ### Cenários de Roteamento
1313
+
1314
+ | Mensagem do Usuário | Rota |
1315
+ |---------------------|------|
1316
+ | "Quero abrir conta para mim" | Triage → Interview PF → Knowledge PF |
1317
+ | "Preciso de maquininha para minha loja" | Triage → Interview PJ → Knowledge PJ |
1318
+ | "Quanto custa o cartão gold?" | Triage → Knowledge PF (direto) |
1319
+ | "Capital de giro para empresa" | Triage → Knowledge PJ (direto) |
1320
+
1321
+ ### Padrão: 1 Interview → 2 Knowledge
1322
+
1323
+ Outro padrão comum é ter um único Interview que pode direcionar para múltiplos Knowledge:
1324
+
1325
+ ```
1326
+ ┌───────────────┐
1327
+ │ Interview │
1328
+ │ (coleta dados)│
1329
+ └───────┬───────┘
1330
+
1331
+ ┌───────┴───────┐
1332
+ ▼ ▼
1333
+ ┌───────────────┐ ┌───────────────┐
1334
+ │ Knowledge │ │ Knowledge │
1335
+ │ Produtos │ │Troubleshooting│
1336
+ └───────────────┘ └───────────────┘
1337
+ ```
1338
+
1339
+ ```python
1340
+ # Um interview que direciona para múltiplos knowledge
1341
+ interview.handoffs = [knowledge_produtos, knowledge_troubleshooting, triage]
1342
+ ```
1343
+
1344
+ 📂 **Exemplo completo**: [example_one_interview_two_knowledge.py](docs/examples/multi_agents/example_one_interview_two_knowledge.py)
1345
+
1346
+ ### Dicas
1347
+
1348
+ 1. **Nomes únicos**: Cada agente precisa de um `name` distinto
1349
+ 2. **Handoffs claros**: Configure quais agentes cada um pode chamar
1350
+ 3. **Keywords no Triage**: Inclua palavras-chave para direcionar corretamente
1351
+ 4. **single_reply**: Use em Knowledge para evitar loops
1352
+
1353
+ ---
1354
+
1042
1355
  ## 📊 Tracing e Monitoramento
1043
1356
 
1044
1357
  ### MonkAI Trace (Recomendado)
@@ -1,7 +1,7 @@
1
1
  atendentepro/README.md,sha256=TAXl5GRjhSwz_I-Dx_eN5JOIcUxuE_dz31iMZ_-OnRY,45390
2
- atendentepro/__init__.py,sha256=ZIN7Nrrx04b5mq4YovL3wk6O6oTY659uffZOI9rYNbE,5820
2
+ atendentepro/__init__.py,sha256=VtJoW9tQ-n-hipu0Lp4gPNWJir1QATouA_CoIl_uY3A,6220
3
3
  atendentepro/license.py,sha256=rlPtysXNqAzEQkP2VjUAVu_nMndhPgfKv1yN2ruUYVI,17570
4
- atendentepro/network.py,sha256=RmNrdf-78qWjlzxB7B8Awa-fJ7ZPQi5iQHZuHBPgVpo,28900
4
+ atendentepro/network.py,sha256=KJG3forldAH0bqMGv4Nga3RxImZUAf0SaV7SB12rovY,29606
5
5
  atendentepro/agents/__init__.py,sha256=OcPhG1Dp6xe49B5YIti4HVmaZDoDIrFLfRa8GmI4jpQ,1638
6
6
  atendentepro/agents/answer.py,sha256=S6wTchNSTMc0h6d89A4jAzoqecFPVqHUrr55-rCM-p4,2494
7
7
  atendentepro/agents/confirmation.py,sha256=bQmIiDaxaCDIWJ3Fxz8h3AHW4kHhwbWSmyLX4y3dtls,2900
@@ -32,12 +32,13 @@ atendentepro/prompts/onboarding.py,sha256=78fSIh2ifsGeoav8DV41_jnyU157c0dtggJujc
32
32
  atendentepro/prompts/triage.py,sha256=bSdEVheGy03r5P6MQuv7NwhN2_wrt0mK80F9f_LskRU,1283
33
33
  atendentepro/templates/__init__.py,sha256=zV1CP2K7_WD219NXl-daTC3Iq8P9sQ7XLmxPEVI2NZg,1575
34
34
  atendentepro/templates/manager.py,sha256=s2ezeyEboeMxdcb6oOADQRAm0ikB8Ru4fYC87gfctU0,28819
35
- atendentepro/utils/__init__.py,sha256=WCJ6_btsLaI6xxHXvNHNue-nKrXWTKscNZGTToQiJ8A,833
35
+ atendentepro/utils/__init__.py,sha256=x2yMUueBilWmI2qASSGbFREeyu0a65r3TuDt0euxNAU,1244
36
36
  atendentepro/utils/openai_client.py,sha256=R0ns7SU36vTgploq14-QJMTke1pPxcAXlENDeoHU0L4,4552
37
37
  atendentepro/utils/tracing.py,sha256=kpTPw1PF4rR1qq1RyBnAaPIQIJRka4RF8MfG_JrRJ7U,8486
38
- atendentepro-0.6.4.dist-info/licenses/LICENSE,sha256=TF6CdXxePoT9DXtPnCejiU5mUwWzrFzd1iyWJyoMauA,983
39
- atendentepro-0.6.4.dist-info/METADATA,sha256=hjjY6FTN3Ht4v_05KO-raxRQkZEuWAxV4ACLtWTnCEs,35702
40
- atendentepro-0.6.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
- atendentepro-0.6.4.dist-info/entry_points.txt,sha256=OP0upzqJF3MLS6VX-M-5BfUwx5YLJO2sJ3YBAp4e6yI,89
42
- atendentepro-0.6.4.dist-info/top_level.txt,sha256=BFasD4SMmgDUmWKlTIZ1PeuukoRBhyiMIz8umKWVCcs,13
43
- atendentepro-0.6.4.dist-info/RECORD,,
38
+ atendentepro/utils/user_loader.py,sha256=J8wd-XF2PZg_i1ped8FI8nmGjmUafXROWJa1tYxSDMI,10623
39
+ atendentepro-0.6.6.dist-info/licenses/LICENSE,sha256=TF6CdXxePoT9DXtPnCejiU5mUwWzrFzd1iyWJyoMauA,983
40
+ atendentepro-0.6.6.dist-info/METADATA,sha256=eVxhCtmNVb_xPm7QLRh63SvREKqPzNq6_76nCsJjNnY,45468
41
+ atendentepro-0.6.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
42
+ atendentepro-0.6.6.dist-info/entry_points.txt,sha256=OP0upzqJF3MLS6VX-M-5BfUwx5YLJO2sJ3YBAp4e6yI,89
43
+ atendentepro-0.6.6.dist-info/top_level.txt,sha256=BFasD4SMmgDUmWKlTIZ1PeuukoRBhyiMIz8umKWVCcs,13
44
+ atendentepro-0.6.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5