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 +14 -0
- atendentepro/network.py +14 -0
- atendentepro/utils/__init__.py +15 -0
- atendentepro/utils/user_loader.py +335 -0
- {atendentepro-0.6.4.dist-info → atendentepro-0.6.6.dist-info}/METADATA +314 -1
- {atendentepro-0.6.4.dist-info → atendentepro-0.6.6.dist-info}/RECORD +10 -9
- {atendentepro-0.6.4.dist-info → atendentepro-0.6.6.dist-info}/WHEEL +1 -1
- {atendentepro-0.6.4.dist-info → atendentepro-0.6.6.dist-info}/entry_points.txt +0 -0
- {atendentepro-0.6.4.dist-info → atendentepro-0.6.6.dist-info}/licenses/LICENSE +0 -0
- {atendentepro-0.6.4.dist-info → atendentepro-0.6.6.dist-info}/top_level.txt +0 -0
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
|
|
atendentepro/utils/__init__.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
39
|
-
atendentepro-0.6.
|
|
40
|
-
atendentepro-0.6.
|
|
41
|
-
atendentepro-0.6.
|
|
42
|
-
atendentepro-0.6.
|
|
43
|
-
atendentepro-0.6.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|