atendentepro 0.6.3__py3-none-any.whl → 0.6.4__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 +10 -0
- atendentepro/models/__init__.py +14 -1
- atendentepro/models/context.py +161 -0
- atendentepro/network.py +143 -50
- atendentepro/templates/__init__.py +8 -0
- atendentepro/templates/manager.py +138 -0
- {atendentepro-0.6.3.dist-info → atendentepro-0.6.4.dist-info}/METADATA +284 -35
- {atendentepro-0.6.3.dist-info → atendentepro-0.6.4.dist-info}/RECORD +12 -12
- {atendentepro-0.6.3.dist-info → atendentepro-0.6.4.dist-info}/WHEEL +0 -0
- {atendentepro-0.6.3.dist-info → atendentepro-0.6.4.dist-info}/entry_points.txt +0 -0
- {atendentepro-0.6.3.dist-info → atendentepro-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {atendentepro-0.6.3.dist-info → atendentepro-0.6.4.dist-info}/top_level.txt +0 -0
atendentepro/__init__.py
CHANGED
|
@@ -80,6 +80,11 @@ from atendentepro.models import (
|
|
|
80
80
|
InterviewOutput,
|
|
81
81
|
KnowledgeToolResult,
|
|
82
82
|
GuardrailValidationOutput,
|
|
83
|
+
# Access filtering
|
|
84
|
+
UserContext,
|
|
85
|
+
AccessFilter,
|
|
86
|
+
FilteredPromptSection,
|
|
87
|
+
FilteredTool,
|
|
83
88
|
)
|
|
84
89
|
|
|
85
90
|
# Agents
|
|
@@ -185,6 +190,11 @@ __all__ = [
|
|
|
185
190
|
"InterviewOutput",
|
|
186
191
|
"KnowledgeToolResult",
|
|
187
192
|
"GuardrailValidationOutput",
|
|
193
|
+
# Access filtering
|
|
194
|
+
"UserContext",
|
|
195
|
+
"AccessFilter",
|
|
196
|
+
"FilteredPromptSection",
|
|
197
|
+
"FilteredTool",
|
|
188
198
|
# Agents
|
|
189
199
|
"create_triage_agent",
|
|
190
200
|
"create_flow_agent",
|
atendentepro/models/__init__.py
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""Models module for AtendentePro library."""
|
|
3
3
|
|
|
4
|
-
from .context import
|
|
4
|
+
from .context import (
|
|
5
|
+
ContextNote,
|
|
6
|
+
UserContext,
|
|
7
|
+
AccessFilter,
|
|
8
|
+
FilteredPromptSection,
|
|
9
|
+
FilteredTool,
|
|
10
|
+
)
|
|
5
11
|
from .outputs import (
|
|
6
12
|
FlowTopic,
|
|
7
13
|
FlowOutput,
|
|
@@ -11,7 +17,14 @@ from .outputs import (
|
|
|
11
17
|
)
|
|
12
18
|
|
|
13
19
|
__all__ = [
|
|
20
|
+
# Context models
|
|
14
21
|
"ContextNote",
|
|
22
|
+
# Access filtering models
|
|
23
|
+
"UserContext",
|
|
24
|
+
"AccessFilter",
|
|
25
|
+
"FilteredPromptSection",
|
|
26
|
+
"FilteredTool",
|
|
27
|
+
# Output models
|
|
15
28
|
"FlowTopic",
|
|
16
29
|
"FlowOutput",
|
|
17
30
|
"InterviewOutput",
|
atendentepro/models/context.py
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
from dataclasses import dataclass, field as dataclass_field
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
8
|
+
|
|
6
9
|
from pydantic import BaseModel, Field
|
|
7
10
|
|
|
8
11
|
|
|
@@ -19,3 +22,161 @@ class ContextNote(BaseModel):
|
|
|
19
22
|
description="Resumos estruturados gerados por agentes anteriores para orientar próximos handoffs.",
|
|
20
23
|
)
|
|
21
24
|
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# ACCESS FILTERING MODELS
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class UserContext:
|
|
32
|
+
"""
|
|
33
|
+
User context for access filtering.
|
|
34
|
+
|
|
35
|
+
Contains user identification and role information used to filter
|
|
36
|
+
access to agents, prompts, and tools.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
user_id: Unique user identifier
|
|
40
|
+
role: User role (e.g., "admin", "cliente", "vendedor")
|
|
41
|
+
metadata: Additional user metadata
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> user = UserContext(
|
|
45
|
+
... user_id="user123",
|
|
46
|
+
... role="vendedor",
|
|
47
|
+
... metadata={"department": "sales", "level": 2}
|
|
48
|
+
... )
|
|
49
|
+
"""
|
|
50
|
+
user_id: Optional[str] = None
|
|
51
|
+
role: Optional[str] = None
|
|
52
|
+
metadata: Dict[str, Any] = dataclass_field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class AccessFilter:
|
|
57
|
+
"""
|
|
58
|
+
Filter for controlling access to agents, prompts, and tools.
|
|
59
|
+
|
|
60
|
+
Supports both whitelist (allowed_*) and blacklist (denied_*) patterns.
|
|
61
|
+
User-level filters take precedence over role-level filters.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
allowed_roles: If set, only these roles can access
|
|
65
|
+
denied_roles: If set, these roles cannot access
|
|
66
|
+
allowed_users: If set, only these users can access (overrides role)
|
|
67
|
+
denied_users: If set, these users cannot access (overrides role)
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> # Only admin and manager can access
|
|
71
|
+
>>> filter1 = AccessFilter(allowed_roles=["admin", "gerente"])
|
|
72
|
+
>>>
|
|
73
|
+
>>> # Everyone except clients
|
|
74
|
+
>>> filter2 = AccessFilter(denied_roles=["cliente"])
|
|
75
|
+
>>>
|
|
76
|
+
>>> # Specific premium users
|
|
77
|
+
>>> filter3 = AccessFilter(allowed_users=["premium_user_1", "premium_user_2"])
|
|
78
|
+
"""
|
|
79
|
+
allowed_roles: Optional[List[str]] = None
|
|
80
|
+
denied_roles: Optional[List[str]] = None
|
|
81
|
+
allowed_users: Optional[List[str]] = None
|
|
82
|
+
denied_users: Optional[List[str]] = None
|
|
83
|
+
|
|
84
|
+
def is_allowed(self, ctx: Optional[UserContext]) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Check if user/role has access.
|
|
87
|
+
|
|
88
|
+
Evaluation order:
|
|
89
|
+
1. If no context provided, allow (no filter applied)
|
|
90
|
+
2. Check denied_users (if user is denied, block)
|
|
91
|
+
3. Check allowed_users (if set and user not in list, block)
|
|
92
|
+
4. Check denied_roles (if role is denied, block)
|
|
93
|
+
5. Check allowed_roles (if set and role not in list, block)
|
|
94
|
+
6. Allow by default
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
ctx: User context to check
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if access is allowed, False otherwise
|
|
101
|
+
"""
|
|
102
|
+
if ctx is None:
|
|
103
|
+
return True # No context = no filter
|
|
104
|
+
|
|
105
|
+
# User-level checks (highest priority)
|
|
106
|
+
if self.denied_users and ctx.user_id in self.denied_users:
|
|
107
|
+
return False
|
|
108
|
+
if self.allowed_users:
|
|
109
|
+
if ctx.user_id in self.allowed_users:
|
|
110
|
+
return True # Explicitly allowed user
|
|
111
|
+
# If allowed_users is set but user not in list, check roles
|
|
112
|
+
# unless no role filters are set
|
|
113
|
+
if not self.allowed_roles and not self.denied_roles:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
# Role-level checks
|
|
117
|
+
if self.denied_roles and ctx.role in self.denied_roles:
|
|
118
|
+
return False
|
|
119
|
+
if self.allowed_roles and ctx.role not in self.allowed_roles:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
return True # No filter matched = allow
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class FilteredPromptSection:
|
|
127
|
+
"""
|
|
128
|
+
Conditional prompt section that appears based on access filter.
|
|
129
|
+
|
|
130
|
+
Allows adding role/user-specific instructions to agent prompts.
|
|
131
|
+
|
|
132
|
+
Attributes:
|
|
133
|
+
content: The prompt content to add
|
|
134
|
+
filter: Access filter determining when to include this section
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> # Section for admins only
|
|
138
|
+
>>> admin_section = FilteredPromptSection(
|
|
139
|
+
... content="## Admin Tools\\nYou can delete users and manage permissions.",
|
|
140
|
+
... filter=AccessFilter(allowed_roles=["admin"])
|
|
141
|
+
... )
|
|
142
|
+
"""
|
|
143
|
+
content: str
|
|
144
|
+
filter: AccessFilter
|
|
145
|
+
|
|
146
|
+
def get_content_if_allowed(self, ctx: Optional[UserContext]) -> str:
|
|
147
|
+
"""Return content if user has access, empty string otherwise."""
|
|
148
|
+
if self.filter.is_allowed(ctx):
|
|
149
|
+
return self.content
|
|
150
|
+
return ""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class FilteredTool:
|
|
155
|
+
"""
|
|
156
|
+
Tool with access filter.
|
|
157
|
+
|
|
158
|
+
Wraps a function_tool with an access filter to control who can use it.
|
|
159
|
+
|
|
160
|
+
Attributes:
|
|
161
|
+
tool: The function_tool callable
|
|
162
|
+
filter: Access filter determining who can use this tool
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
>>> @function_tool
|
|
166
|
+
... def delete_user(user_id: str) -> str:
|
|
167
|
+
... return f"User {user_id} deleted"
|
|
168
|
+
>>>
|
|
169
|
+
>>> admin_tool = FilteredTool(
|
|
170
|
+
... tool=delete_user,
|
|
171
|
+
... filter=AccessFilter(allowed_roles=["admin"])
|
|
172
|
+
... )
|
|
173
|
+
"""
|
|
174
|
+
tool: Callable
|
|
175
|
+
filter: AccessFilter
|
|
176
|
+
|
|
177
|
+
def get_tool_if_allowed(self, ctx: Optional[UserContext]) -> Optional[Callable]:
|
|
178
|
+
"""Return tool if user has access, None otherwise."""
|
|
179
|
+
if self.filter.is_allowed(ctx):
|
|
180
|
+
return self.tool
|
|
181
|
+
return None
|
|
182
|
+
|
atendentepro/network.py
CHANGED
|
@@ -122,6 +122,15 @@ from atendentepro.templates import (
|
|
|
122
122
|
load_knowledge_config,
|
|
123
123
|
load_confirmation_config,
|
|
124
124
|
load_onboarding_config,
|
|
125
|
+
load_access_config,
|
|
126
|
+
AccessConfig,
|
|
127
|
+
AccessFilterConfig,
|
|
128
|
+
)
|
|
129
|
+
from atendentepro.models import (
|
|
130
|
+
UserContext,
|
|
131
|
+
AccessFilter,
|
|
132
|
+
FilteredPromptSection,
|
|
133
|
+
FilteredTool,
|
|
125
134
|
)
|
|
126
135
|
|
|
127
136
|
|
|
@@ -202,6 +211,11 @@ def create_standard_network(
|
|
|
202
211
|
# Single reply mode (auto-return to triage after one response)
|
|
203
212
|
global_single_reply: bool = False,
|
|
204
213
|
single_reply_agents: Optional[Dict[str, bool]] = None,
|
|
214
|
+
# Access filtering (role/user based)
|
|
215
|
+
user_context: Optional[UserContext] = None,
|
|
216
|
+
agent_filters: Optional[Dict[str, AccessFilter]] = None,
|
|
217
|
+
conditional_prompts: Optional[Dict[str, List[FilteredPromptSection]]] = None,
|
|
218
|
+
filtered_tools: Optional[Dict[str, List[FilteredTool]]] = None,
|
|
205
219
|
) -> AgentNetwork:
|
|
206
220
|
"""
|
|
207
221
|
Create a standard agent network with proper handoff configuration.
|
|
@@ -241,6 +255,10 @@ def create_standard_network(
|
|
|
241
255
|
agent_styles: Dict mapping agent names to specific styles (overrides global).
|
|
242
256
|
global_single_reply: If True, all agents respond once then transfer to triage.
|
|
243
257
|
single_reply_agents: Dict mapping agent names to single_reply mode (overrides global).
|
|
258
|
+
user_context: User context for access filtering (user_id, role, metadata).
|
|
259
|
+
agent_filters: Dict mapping agent names to AccessFilter (controls agent access).
|
|
260
|
+
conditional_prompts: Dict mapping agent names to list of FilteredPromptSection.
|
|
261
|
+
filtered_tools: Dict mapping agent names to list of FilteredTool.
|
|
244
262
|
|
|
245
263
|
Returns:
|
|
246
264
|
Configured AgentNetwork instance.
|
|
@@ -256,15 +274,27 @@ def create_standard_network(
|
|
|
256
274
|
include_knowledge=False,
|
|
257
275
|
)
|
|
258
276
|
|
|
259
|
-
# Create
|
|
277
|
+
# Create network with role-based access filtering
|
|
278
|
+
from atendentepro import UserContext, AccessFilter, FilteredPromptSection
|
|
279
|
+
|
|
280
|
+
user = UserContext(user_id="user123", role="vendedor")
|
|
281
|
+
|
|
260
282
|
network = create_standard_network(
|
|
261
283
|
templates_root=Path("templates"),
|
|
262
284
|
client="my_client",
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
285
|
+
user_context=user,
|
|
286
|
+
agent_filters={
|
|
287
|
+
"knowledge": AccessFilter(allowed_roles=["admin", "vendedor"]),
|
|
288
|
+
"escalation": AccessFilter(denied_roles=["cliente"]),
|
|
289
|
+
},
|
|
290
|
+
conditional_prompts={
|
|
291
|
+
"knowledge": [
|
|
292
|
+
FilteredPromptSection(
|
|
293
|
+
content="\\n## Descontos\\nVocê pode oferecer até 15% de desconto.",
|
|
294
|
+
filter=AccessFilter(allowed_roles=["vendedor"]),
|
|
295
|
+
),
|
|
296
|
+
],
|
|
297
|
+
},
|
|
268
298
|
)
|
|
269
299
|
|
|
270
300
|
# Create network with custom communication style
|
|
@@ -276,23 +306,6 @@ def create_standard_network(
|
|
|
276
306
|
language_style="formal",
|
|
277
307
|
response_length="moderado",
|
|
278
308
|
),
|
|
279
|
-
agent_styles={
|
|
280
|
-
"escalation": AgentStyle(
|
|
281
|
-
tone="empático e acolhedor",
|
|
282
|
-
custom_rules="Demonstre compreensão pela situação.",
|
|
283
|
-
),
|
|
284
|
-
},
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
# Create network with single reply mode for specific agents
|
|
288
|
-
network = create_standard_network(
|
|
289
|
-
templates_root=Path("templates"),
|
|
290
|
-
client="my_client",
|
|
291
|
-
global_single_reply=False, # Not all agents
|
|
292
|
-
single_reply_agents={
|
|
293
|
-
"knowledge": True, # Knowledge responds once
|
|
294
|
-
"confirmation": True, # Confirmation responds once
|
|
295
|
-
},
|
|
296
309
|
)
|
|
297
310
|
"""
|
|
298
311
|
# Verificar licença
|
|
@@ -301,6 +314,79 @@ def create_standard_network(
|
|
|
301
314
|
# Configure template manager
|
|
302
315
|
manager = configure_template_manager(templates_root, default_client=client)
|
|
303
316
|
|
|
317
|
+
# Load access config from YAML if not provided via code
|
|
318
|
+
yaml_access_config = None
|
|
319
|
+
try:
|
|
320
|
+
yaml_access_config = load_access_config(client)
|
|
321
|
+
except FileNotFoundError:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
# Helper function to check if agent is allowed for user
|
|
325
|
+
def is_agent_allowed(agent_name: str) -> bool:
|
|
326
|
+
"""Check if agent should be included based on user context."""
|
|
327
|
+
if user_context is None:
|
|
328
|
+
return True # No filtering if no user context
|
|
329
|
+
|
|
330
|
+
# Code-level filter takes precedence
|
|
331
|
+
if agent_filters and agent_name in agent_filters:
|
|
332
|
+
return agent_filters[agent_name].is_allowed(user_context)
|
|
333
|
+
|
|
334
|
+
# Fall back to YAML config
|
|
335
|
+
if yaml_access_config:
|
|
336
|
+
yaml_filter = yaml_access_config.get_agent_filter(agent_name)
|
|
337
|
+
if yaml_filter:
|
|
338
|
+
# Convert AccessFilterConfig to AccessFilter
|
|
339
|
+
access_filter = AccessFilter(
|
|
340
|
+
allowed_roles=yaml_filter.allowed_roles,
|
|
341
|
+
denied_roles=yaml_filter.denied_roles,
|
|
342
|
+
allowed_users=yaml_filter.allowed_users,
|
|
343
|
+
denied_users=yaml_filter.denied_users,
|
|
344
|
+
)
|
|
345
|
+
return access_filter.is_allowed(user_context)
|
|
346
|
+
|
|
347
|
+
return True # No filter = allowed
|
|
348
|
+
|
|
349
|
+
# Helper function to get conditional prompts for agent
|
|
350
|
+
def get_conditional_prompts_for_agent(agent_name: str) -> str:
|
|
351
|
+
"""Get all allowed conditional prompt sections for an agent."""
|
|
352
|
+
sections = []
|
|
353
|
+
|
|
354
|
+
# Code-level prompts take precedence
|
|
355
|
+
if conditional_prompts and agent_name in conditional_prompts:
|
|
356
|
+
for section in conditional_prompts[agent_name]:
|
|
357
|
+
content = section.get_content_if_allowed(user_context)
|
|
358
|
+
if content:
|
|
359
|
+
sections.append(content)
|
|
360
|
+
|
|
361
|
+
# Add YAML-level prompts
|
|
362
|
+
if yaml_access_config:
|
|
363
|
+
for prompt_cfg in yaml_access_config.get_conditional_prompts(agent_name):
|
|
364
|
+
# Convert to AccessFilter and check
|
|
365
|
+
access_filter = AccessFilter(
|
|
366
|
+
allowed_roles=prompt_cfg.filter.allowed_roles,
|
|
367
|
+
denied_roles=prompt_cfg.filter.denied_roles,
|
|
368
|
+
allowed_users=prompt_cfg.filter.allowed_users,
|
|
369
|
+
denied_users=prompt_cfg.filter.denied_users,
|
|
370
|
+
)
|
|
371
|
+
if access_filter.is_allowed(user_context):
|
|
372
|
+
sections.append(prompt_cfg.content)
|
|
373
|
+
|
|
374
|
+
return "\n".join(sections)
|
|
375
|
+
|
|
376
|
+
# Helper function to get filtered tools for agent
|
|
377
|
+
def get_filtered_tools_for_agent(agent_name: str, base_tools: List) -> List:
|
|
378
|
+
"""Get tools filtered by user context."""
|
|
379
|
+
result_tools = list(base_tools) if base_tools else []
|
|
380
|
+
|
|
381
|
+
# Add code-level filtered tools
|
|
382
|
+
if filtered_tools and agent_name in filtered_tools:
|
|
383
|
+
for ft in filtered_tools[agent_name]:
|
|
384
|
+
tool = ft.get_tool_if_allowed(user_context)
|
|
385
|
+
if tool:
|
|
386
|
+
result_tools.append(tool)
|
|
387
|
+
|
|
388
|
+
return result_tools
|
|
389
|
+
|
|
304
390
|
# Load configurations
|
|
305
391
|
try:
|
|
306
392
|
flow_config = load_flow_config(client)
|
|
@@ -381,47 +467,54 @@ def create_standard_network(
|
|
|
381
467
|
# Fall back to global setting
|
|
382
468
|
return global_single_reply
|
|
383
469
|
|
|
470
|
+
# Helper to build full style instructions with conditional prompts
|
|
471
|
+
def get_full_instructions(agent_name: str) -> str:
|
|
472
|
+
"""Get style + conditional prompt instructions for an agent."""
|
|
473
|
+
style = get_style_for_agent(agent_name)
|
|
474
|
+
conditional = get_conditional_prompts_for_agent(agent_name)
|
|
475
|
+
return f"{style}{conditional}" if conditional else style
|
|
476
|
+
|
|
384
477
|
# Create agents without handoffs first
|
|
385
478
|
# Triage is always created (entry point)
|
|
386
479
|
triage = create_triage_agent(
|
|
387
480
|
keywords_text=triage_keywords,
|
|
388
481
|
guardrails=get_guardrails_for_agent("Triage Agent", templates_root),
|
|
389
|
-
style_instructions=
|
|
482
|
+
style_instructions=get_full_instructions("triage"),
|
|
390
483
|
)
|
|
391
484
|
|
|
392
|
-
# Create optional agents based on include flags
|
|
485
|
+
# Create optional agents based on include flags AND access filters
|
|
393
486
|
flow = None
|
|
394
|
-
if include_flow:
|
|
487
|
+
if include_flow and is_agent_allowed("flow"):
|
|
395
488
|
flow = create_flow_agent(
|
|
396
489
|
flow_template=flow_template,
|
|
397
490
|
flow_keywords=flow_keywords,
|
|
398
491
|
guardrails=get_guardrails_for_agent("Flow Agent", templates_root),
|
|
399
|
-
style_instructions=
|
|
492
|
+
style_instructions=get_full_instructions("flow"),
|
|
400
493
|
single_reply=get_single_reply_for_agent("flow"),
|
|
401
494
|
)
|
|
402
495
|
|
|
403
496
|
interview = None
|
|
404
|
-
if include_interview:
|
|
497
|
+
if include_interview and is_agent_allowed("interview"):
|
|
405
498
|
interview = create_interview_agent(
|
|
406
499
|
interview_template=flow_template,
|
|
407
500
|
interview_questions=interview_questions,
|
|
408
|
-
tools=tools.get("interview", []),
|
|
501
|
+
tools=get_filtered_tools_for_agent("interview", tools.get("interview", [])),
|
|
409
502
|
guardrails=get_guardrails_for_agent("Interview Agent", templates_root),
|
|
410
|
-
style_instructions=
|
|
503
|
+
style_instructions=get_full_instructions("interview"),
|
|
411
504
|
single_reply=get_single_reply_for_agent("interview"),
|
|
412
505
|
)
|
|
413
506
|
|
|
414
507
|
answer = None
|
|
415
|
-
if include_answer:
|
|
508
|
+
if include_answer and is_agent_allowed("answer"):
|
|
416
509
|
answer = create_answer_agent(
|
|
417
510
|
answer_template="",
|
|
418
511
|
guardrails=get_guardrails_for_agent("Answer Agent", templates_root),
|
|
419
|
-
style_instructions=
|
|
512
|
+
style_instructions=get_full_instructions("answer"),
|
|
420
513
|
single_reply=get_single_reply_for_agent("answer"),
|
|
421
514
|
)
|
|
422
515
|
|
|
423
516
|
knowledge = None
|
|
424
|
-
if include_knowledge:
|
|
517
|
+
if include_knowledge and is_agent_allowed("knowledge"):
|
|
425
518
|
knowledge = create_knowledge_agent(
|
|
426
519
|
knowledge_about=knowledge_about,
|
|
427
520
|
knowledge_template=knowledge_template,
|
|
@@ -429,52 +522,52 @@ def create_standard_network(
|
|
|
429
522
|
embeddings_path=embeddings_path,
|
|
430
523
|
data_sources_description=data_sources_description,
|
|
431
524
|
include_rag_tool=has_embeddings,
|
|
432
|
-
tools=tools.get("knowledge", []),
|
|
525
|
+
tools=get_filtered_tools_for_agent("knowledge", tools.get("knowledge", [])),
|
|
433
526
|
guardrails=get_guardrails_for_agent("Knowledge Agent", templates_root),
|
|
434
|
-
style_instructions=
|
|
527
|
+
style_instructions=get_full_instructions("knowledge"),
|
|
435
528
|
single_reply=get_single_reply_for_agent("knowledge"),
|
|
436
529
|
)
|
|
437
530
|
|
|
438
531
|
confirmation = None
|
|
439
|
-
if include_confirmation:
|
|
532
|
+
if include_confirmation and is_agent_allowed("confirmation"):
|
|
440
533
|
confirmation = create_confirmation_agent(
|
|
441
534
|
confirmation_about=confirmation_about,
|
|
442
535
|
confirmation_template=confirmation_template,
|
|
443
536
|
confirmation_format=confirmation_format,
|
|
444
537
|
guardrails=get_guardrails_for_agent("Confirmation Agent", templates_root),
|
|
445
|
-
style_instructions=
|
|
538
|
+
style_instructions=get_full_instructions("confirmation"),
|
|
446
539
|
single_reply=get_single_reply_for_agent("confirmation"),
|
|
447
540
|
)
|
|
448
541
|
|
|
449
542
|
usage = None
|
|
450
|
-
if include_usage:
|
|
543
|
+
if include_usage and is_agent_allowed("usage"):
|
|
451
544
|
usage = create_usage_agent(
|
|
452
545
|
guardrails=get_guardrails_for_agent("Usage Agent", templates_root),
|
|
453
|
-
style_instructions=
|
|
546
|
+
style_instructions=get_full_instructions("usage"),
|
|
454
547
|
single_reply=get_single_reply_for_agent("usage"),
|
|
455
548
|
)
|
|
456
549
|
|
|
457
|
-
# Create escalation agent if requested
|
|
550
|
+
# Create escalation agent if requested and allowed
|
|
458
551
|
escalation = None
|
|
459
|
-
if include_escalation:
|
|
552
|
+
if include_escalation and is_agent_allowed("escalation"):
|
|
460
553
|
escalation = create_escalation_agent(
|
|
461
554
|
escalation_channels=escalation_channels,
|
|
462
|
-
tools=tools.get("escalation", []),
|
|
555
|
+
tools=get_filtered_tools_for_agent("escalation", tools.get("escalation", [])),
|
|
463
556
|
guardrails=get_guardrails_for_agent("Escalation Agent", templates_root),
|
|
464
|
-
style_instructions=
|
|
557
|
+
style_instructions=get_full_instructions("escalation"),
|
|
465
558
|
single_reply=get_single_reply_for_agent("escalation"),
|
|
466
559
|
)
|
|
467
560
|
|
|
468
|
-
# Create feedback agent if requested
|
|
561
|
+
# Create feedback agent if requested and allowed
|
|
469
562
|
feedback = None
|
|
470
|
-
if include_feedback:
|
|
563
|
+
if include_feedback and is_agent_allowed("feedback"):
|
|
471
564
|
feedback = create_feedback_agent(
|
|
472
565
|
protocol_prefix=feedback_protocol_prefix,
|
|
473
566
|
email_brand_color=feedback_brand_color,
|
|
474
567
|
email_brand_name=feedback_brand_name,
|
|
475
|
-
tools=tools.get("feedback", []),
|
|
568
|
+
tools=get_filtered_tools_for_agent("feedback", tools.get("feedback", [])),
|
|
476
569
|
guardrails=get_guardrails_for_agent("Feedback Agent", templates_root),
|
|
477
|
-
style_instructions=
|
|
570
|
+
style_instructions=get_full_instructions("feedback"),
|
|
478
571
|
single_reply=get_single_reply_for_agent("feedback"),
|
|
479
572
|
)
|
|
480
573
|
|
|
@@ -566,7 +659,7 @@ def create_standard_network(
|
|
|
566
659
|
)
|
|
567
660
|
|
|
568
661
|
# Add onboarding if requested
|
|
569
|
-
if include_onboarding:
|
|
662
|
+
if include_onboarding and is_agent_allowed("onboarding"):
|
|
570
663
|
try:
|
|
571
664
|
onboarding_config = load_onboarding_config(client)
|
|
572
665
|
from atendentepro.prompts.onboarding import OnboardingField as PromptField
|
|
@@ -584,9 +677,9 @@ def create_standard_network(
|
|
|
584
677
|
|
|
585
678
|
onboarding = create_onboarding_agent(
|
|
586
679
|
required_fields=fields,
|
|
587
|
-
tools=tools.get("onboarding", []),
|
|
680
|
+
tools=get_filtered_tools_for_agent("onboarding", tools.get("onboarding", [])),
|
|
588
681
|
guardrails=get_guardrails_for_agent("Onboarding Agent", templates_root),
|
|
589
|
-
style_instructions=
|
|
682
|
+
style_instructions=get_full_instructions("onboarding"),
|
|
590
683
|
single_reply=get_single_reply_for_agent("onboarding"),
|
|
591
684
|
)
|
|
592
685
|
|
|
@@ -16,6 +16,7 @@ from .manager import (
|
|
|
16
16
|
load_onboarding_config,
|
|
17
17
|
load_style_config,
|
|
18
18
|
load_single_reply_config,
|
|
19
|
+
load_access_config,
|
|
19
20
|
FlowConfig,
|
|
20
21
|
InterviewConfig,
|
|
21
22
|
TriageConfig,
|
|
@@ -25,6 +26,9 @@ from .manager import (
|
|
|
25
26
|
StyleConfig,
|
|
26
27
|
AgentStyleConfig,
|
|
27
28
|
SingleReplyConfig,
|
|
29
|
+
AccessConfig,
|
|
30
|
+
AccessFilterConfig,
|
|
31
|
+
ConditionalPromptConfig,
|
|
28
32
|
DataSourceConfig,
|
|
29
33
|
DataSourceColumn,
|
|
30
34
|
DocumentConfig,
|
|
@@ -45,6 +49,7 @@ __all__ = [
|
|
|
45
49
|
"load_onboarding_config",
|
|
46
50
|
"load_style_config",
|
|
47
51
|
"load_single_reply_config",
|
|
52
|
+
"load_access_config",
|
|
48
53
|
"FlowConfig",
|
|
49
54
|
"InterviewConfig",
|
|
50
55
|
"TriageConfig",
|
|
@@ -54,6 +59,9 @@ __all__ = [
|
|
|
54
59
|
"StyleConfig",
|
|
55
60
|
"AgentStyleConfig",
|
|
56
61
|
"SingleReplyConfig",
|
|
62
|
+
"AccessConfig",
|
|
63
|
+
"AccessFilterConfig",
|
|
64
|
+
"ConditionalPromptConfig",
|
|
57
65
|
"DataSourceConfig",
|
|
58
66
|
"DataSourceColumn",
|
|
59
67
|
"DocumentConfig",
|
|
@@ -434,6 +434,133 @@ class SingleReplyConfig(BaseModel):
|
|
|
434
434
|
return self.global_enabled
|
|
435
435
|
|
|
436
436
|
|
|
437
|
+
# =============================================================================
|
|
438
|
+
# ACCESS FILTER CONFIGURATION (Role/User based access control)
|
|
439
|
+
# =============================================================================
|
|
440
|
+
|
|
441
|
+
class AccessFilterConfig(BaseModel):
|
|
442
|
+
"""Configuration for a single access filter."""
|
|
443
|
+
|
|
444
|
+
allowed_roles: Optional[List[str]] = None
|
|
445
|
+
denied_roles: Optional[List[str]] = None
|
|
446
|
+
allowed_users: Optional[List[str]] = None
|
|
447
|
+
denied_users: Optional[List[str]] = None
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class ConditionalPromptConfig(BaseModel):
|
|
451
|
+
"""Configuration for a conditional prompt section."""
|
|
452
|
+
|
|
453
|
+
content: str = ""
|
|
454
|
+
filter: AccessFilterConfig = Field(default_factory=AccessFilterConfig)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class AccessConfig(BaseModel):
|
|
458
|
+
"""
|
|
459
|
+
Configuration model for role/user based access control.
|
|
460
|
+
|
|
461
|
+
Allows controlling access to agents, prompts, and tools based on
|
|
462
|
+
user identity or role. Supports both whitelist and blacklist patterns.
|
|
463
|
+
|
|
464
|
+
Example YAML:
|
|
465
|
+
# Filter entire agents by role
|
|
466
|
+
agent_filters:
|
|
467
|
+
knowledge:
|
|
468
|
+
allowed_roles: ["admin", "vendedor"]
|
|
469
|
+
escalation:
|
|
470
|
+
denied_roles: ["cliente"]
|
|
471
|
+
|
|
472
|
+
# Conditional prompt sections
|
|
473
|
+
conditional_prompts:
|
|
474
|
+
knowledge:
|
|
475
|
+
- content: |
|
|
476
|
+
## Admin Tools
|
|
477
|
+
You have access to admin features.
|
|
478
|
+
filter:
|
|
479
|
+
allowed_roles: ["admin"]
|
|
480
|
+
- content: |
|
|
481
|
+
## Sales Tools
|
|
482
|
+
You can offer up to 15% discount.
|
|
483
|
+
filter:
|
|
484
|
+
allowed_roles: ["vendedor"]
|
|
485
|
+
|
|
486
|
+
# Tool access control
|
|
487
|
+
tool_access:
|
|
488
|
+
delete_user:
|
|
489
|
+
allowed_roles: ["admin"]
|
|
490
|
+
approve_discount:
|
|
491
|
+
allowed_roles: ["gerente", "admin"]
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
agent_filters: Dict[str, AccessFilterConfig] = Field(default_factory=dict)
|
|
495
|
+
conditional_prompts: Dict[str, List[ConditionalPromptConfig]] = Field(default_factory=dict)
|
|
496
|
+
tool_access: Dict[str, AccessFilterConfig] = Field(default_factory=dict)
|
|
497
|
+
|
|
498
|
+
@classmethod
|
|
499
|
+
@lru_cache(maxsize=4)
|
|
500
|
+
def load(cls, path: Path) -> "AccessConfig":
|
|
501
|
+
"""Load access configuration from YAML file."""
|
|
502
|
+
if not path.exists():
|
|
503
|
+
raise FileNotFoundError(f"Access config not found at {path}")
|
|
504
|
+
|
|
505
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
506
|
+
data = yaml.safe_load(f) or {}
|
|
507
|
+
|
|
508
|
+
# Parse agent filters
|
|
509
|
+
agent_filters = {}
|
|
510
|
+
for agent_name, filter_data in data.get("agent_filters", {}).items():
|
|
511
|
+
agent_filters[agent_name] = AccessFilterConfig(
|
|
512
|
+
allowed_roles=filter_data.get("allowed_roles"),
|
|
513
|
+
denied_roles=filter_data.get("denied_roles"),
|
|
514
|
+
allowed_users=filter_data.get("allowed_users"),
|
|
515
|
+
denied_users=filter_data.get("denied_users"),
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Parse conditional prompts
|
|
519
|
+
conditional_prompts = {}
|
|
520
|
+
for agent_name, prompts_data in data.get("conditional_prompts", {}).items():
|
|
521
|
+
prompts_list = []
|
|
522
|
+
for prompt_data in prompts_data:
|
|
523
|
+
filter_data = prompt_data.get("filter", {})
|
|
524
|
+
prompts_list.append(ConditionalPromptConfig(
|
|
525
|
+
content=prompt_data.get("content", ""),
|
|
526
|
+
filter=AccessFilterConfig(
|
|
527
|
+
allowed_roles=filter_data.get("allowed_roles"),
|
|
528
|
+
denied_roles=filter_data.get("denied_roles"),
|
|
529
|
+
allowed_users=filter_data.get("allowed_users"),
|
|
530
|
+
denied_users=filter_data.get("denied_users"),
|
|
531
|
+
),
|
|
532
|
+
))
|
|
533
|
+
conditional_prompts[agent_name] = prompts_list
|
|
534
|
+
|
|
535
|
+
# Parse tool access
|
|
536
|
+
tool_access = {}
|
|
537
|
+
for tool_name, filter_data in data.get("tool_access", {}).items():
|
|
538
|
+
tool_access[tool_name] = AccessFilterConfig(
|
|
539
|
+
allowed_roles=filter_data.get("allowed_roles"),
|
|
540
|
+
denied_roles=filter_data.get("denied_roles"),
|
|
541
|
+
allowed_users=filter_data.get("allowed_users"),
|
|
542
|
+
denied_users=filter_data.get("denied_users"),
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
return cls(
|
|
546
|
+
agent_filters=agent_filters,
|
|
547
|
+
conditional_prompts=conditional_prompts,
|
|
548
|
+
tool_access=tool_access,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
def get_agent_filter(self, agent_name: str) -> Optional[AccessFilterConfig]:
|
|
552
|
+
"""Get access filter for a specific agent."""
|
|
553
|
+
return self.agent_filters.get(agent_name)
|
|
554
|
+
|
|
555
|
+
def get_conditional_prompts(self, agent_name: str) -> List[ConditionalPromptConfig]:
|
|
556
|
+
"""Get conditional prompts for a specific agent."""
|
|
557
|
+
return self.conditional_prompts.get(agent_name, [])
|
|
558
|
+
|
|
559
|
+
def get_tool_filter(self, tool_name: str) -> Optional[AccessFilterConfig]:
|
|
560
|
+
"""Get access filter for a specific tool."""
|
|
561
|
+
return self.tool_access.get(tool_name)
|
|
562
|
+
|
|
563
|
+
|
|
437
564
|
class OnboardingConfig(BaseModel):
|
|
438
565
|
"""Configuration model for Onboarding Agent."""
|
|
439
566
|
|
|
@@ -562,6 +689,11 @@ class TemplateManager:
|
|
|
562
689
|
folder = self.get_template_folder(client)
|
|
563
690
|
return SingleReplyConfig.load(folder / "single_reply_config.yaml")
|
|
564
691
|
|
|
692
|
+
def load_access_config(self, client: Optional[str] = None) -> AccessConfig:
|
|
693
|
+
"""Load access configuration for the specified client."""
|
|
694
|
+
folder = self.get_template_folder(client)
|
|
695
|
+
return AccessConfig.load(folder / "access_config.yaml")
|
|
696
|
+
|
|
565
697
|
def clear_caches(self) -> None:
|
|
566
698
|
"""Clear all configuration caches."""
|
|
567
699
|
FlowConfig.load.cache_clear()
|
|
@@ -572,6 +704,7 @@ class TemplateManager:
|
|
|
572
704
|
OnboardingConfig.load.cache_clear()
|
|
573
705
|
StyleConfig.load.cache_clear()
|
|
574
706
|
SingleReplyConfig.load.cache_clear()
|
|
707
|
+
AccessConfig.load.cache_clear()
|
|
575
708
|
|
|
576
709
|
|
|
577
710
|
def get_template_manager() -> TemplateManager:
|
|
@@ -670,3 +803,8 @@ def load_single_reply_config(client: Optional[str] = None) -> SingleReplyConfig:
|
|
|
670
803
|
"""Load single reply configuration for the specified client."""
|
|
671
804
|
return get_template_manager().load_single_reply_config(client)
|
|
672
805
|
|
|
806
|
+
|
|
807
|
+
def load_access_config(client: Optional[str] = None) -> AccessConfig:
|
|
808
|
+
"""Load access configuration for the specified client."""
|
|
809
|
+
return get_template_manager().load_access_config(client)
|
|
810
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atendentepro
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
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>
|
|
@@ -95,6 +95,7 @@ Plataforma que unifica múltiplos agentes especializados para resolver demandas
|
|
|
95
95
|
- [Fluxo de Handoffs](#-fluxo-de-handoffs)
|
|
96
96
|
- [Estilo de Comunicação](#-estilo-de-comunicação-agentstyle)
|
|
97
97
|
- [Single Reply Mode](#-single-reply-mode)
|
|
98
|
+
- [Filtros de Acesso](#-filtros-de-acesso-roleuser)
|
|
98
99
|
- [Tracing e Monitoramento](#-tracing-e-monitoramento)
|
|
99
100
|
- [Suporte](#-suporte)
|
|
100
101
|
|
|
@@ -712,80 +713,328 @@ agents:
|
|
|
712
713
|
|
|
713
714
|
O **Single Reply Mode** permite configurar agentes para responderem apenas uma vez e automaticamente transferirem de volta para o Triage. Isso evita que a conversa fique "presa" em um agente específico.
|
|
714
715
|
|
|
716
|
+
📂 **Exemplos completos**: [docs/examples/single_reply/](docs/examples/single_reply/)
|
|
717
|
+
|
|
715
718
|
### Quando Usar
|
|
716
719
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
+
| Cenário | Recomendação |
|
|
721
|
+
|---------|--------------|
|
|
722
|
+
| **Chatbots de alto volume** | ✅ Ativar para respostas rápidas |
|
|
723
|
+
| **FAQ simples** | ✅ Knowledge com single_reply |
|
|
724
|
+
| **Coleta de dados** | ❌ Interview precisa múltiplas interações |
|
|
725
|
+
| **Onboarding** | ❌ Precisa guiar o usuário em etapas |
|
|
726
|
+
| **Confirmações** | ✅ Confirma e volta ao Triage |
|
|
720
727
|
|
|
721
|
-
### Via Código
|
|
728
|
+
### Exemplo 1: FAQ Bot (Via Código)
|
|
729
|
+
|
|
730
|
+
Chatbot otimizado para perguntas frequentes:
|
|
722
731
|
|
|
723
732
|
```python
|
|
724
733
|
from pathlib import Path
|
|
725
734
|
from atendentepro import create_standard_network
|
|
726
735
|
|
|
727
|
-
#
|
|
736
|
+
# FAQ Bot: Knowledge e Answer respondem uma vez
|
|
728
737
|
network = create_standard_network(
|
|
729
738
|
templates_root=Path("./meu_cliente"),
|
|
730
739
|
client="config",
|
|
731
|
-
global_single_reply=
|
|
740
|
+
global_single_reply=False,
|
|
741
|
+
single_reply_agents={
|
|
742
|
+
"knowledge": True, # FAQ: responde e volta
|
|
743
|
+
"answer": True, # Perguntas gerais: responde e volta
|
|
744
|
+
"flow": True, # Menu: apresenta e volta
|
|
745
|
+
},
|
|
732
746
|
)
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Exemplo 2: Bot de Leads (Via Código)
|
|
733
750
|
|
|
734
|
-
|
|
751
|
+
Bot que coleta dados mas responde dúvidas rapidamente:
|
|
752
|
+
|
|
753
|
+
```python
|
|
735
754
|
network = create_standard_network(
|
|
736
755
|
templates_root=Path("./meu_cliente"),
|
|
737
756
|
client="config",
|
|
738
757
|
global_single_reply=False,
|
|
739
758
|
single_reply_agents={
|
|
740
|
-
|
|
741
|
-
"
|
|
742
|
-
|
|
743
|
-
#
|
|
759
|
+
# Interview PRECISA de múltiplas interações para coletar dados
|
|
760
|
+
"interview": False,
|
|
761
|
+
|
|
762
|
+
# Outros agentes podem ser rápidos
|
|
763
|
+
"knowledge": True, # Tira dúvidas sobre produto
|
|
764
|
+
"answer": True, # Responde perguntas
|
|
765
|
+
"confirmation": True, # Confirma cadastro
|
|
744
766
|
},
|
|
745
767
|
)
|
|
746
768
|
```
|
|
747
769
|
|
|
770
|
+
### Exemplo 3: Ativar para TODOS os agentes
|
|
771
|
+
|
|
772
|
+
```python
|
|
773
|
+
network = create_standard_network(
|
|
774
|
+
templates_root=Path("./meu_cliente"),
|
|
775
|
+
client="config",
|
|
776
|
+
global_single_reply=True, # Todos respondem uma vez
|
|
777
|
+
)
|
|
778
|
+
```
|
|
779
|
+
|
|
748
780
|
### Via YAML (single_reply_config.yaml)
|
|
749
781
|
|
|
750
782
|
Crie o arquivo `single_reply_config.yaml` na pasta do cliente:
|
|
751
783
|
|
|
752
784
|
```yaml
|
|
753
|
-
# Global:
|
|
785
|
+
# Global: se true, TODOS os agentes respondem apenas uma vez
|
|
754
786
|
global: false
|
|
755
787
|
|
|
756
|
-
# Configuração por agente
|
|
788
|
+
# Configuração por agente (sobrescreve global)
|
|
757
789
|
agents:
|
|
758
|
-
# Agentes
|
|
759
|
-
knowledge: true
|
|
760
|
-
|
|
761
|
-
|
|
790
|
+
# Agentes de consulta: respondem uma vez
|
|
791
|
+
knowledge: true # FAQ: responde e volta
|
|
792
|
+
answer: true # Perguntas: responde e volta
|
|
793
|
+
confirmation: true # Confirma e volta
|
|
794
|
+
usage: true # Explica uso e volta
|
|
795
|
+
|
|
796
|
+
# Agentes de coleta: múltiplas interações
|
|
797
|
+
interview: false # Precisa coletar dados
|
|
798
|
+
onboarding: false # Precisa guiar usuário
|
|
762
799
|
|
|
763
|
-
#
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
800
|
+
# Opcionais
|
|
801
|
+
flow: true # Menu: apresenta e volta
|
|
802
|
+
escalation: true # Registra e volta
|
|
803
|
+
feedback: true # Coleta feedback e volta
|
|
767
804
|
```
|
|
768
805
|
|
|
769
|
-
###
|
|
806
|
+
### Fluxo Visual
|
|
770
807
|
|
|
771
|
-
|
|
808
|
+
**Com single_reply=True:**
|
|
772
809
|
|
|
773
810
|
```
|
|
774
|
-
[Usuário
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
811
|
+
[Usuário: "Qual o preço?"]
|
|
812
|
+
↓
|
|
813
|
+
[Triage] → detecta consulta
|
|
814
|
+
↓
|
|
815
|
+
[Knowledge] → responde: "R$ 99,90"
|
|
816
|
+
↓
|
|
817
|
+
[Triage] ← retorno AUTOMÁTICO
|
|
818
|
+
↓
|
|
819
|
+
[Usuário: "E a entrega?"]
|
|
820
|
+
↓
|
|
821
|
+
[Triage] → nova análise (ciclo reinicia)
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**Com single_reply=False (padrão):**
|
|
825
|
+
|
|
779
826
|
```
|
|
827
|
+
[Usuário: "Qual o preço?"]
|
|
828
|
+
↓
|
|
829
|
+
[Triage] → detecta consulta
|
|
830
|
+
↓
|
|
831
|
+
[Knowledge] → responde: "R$ 99,90"
|
|
832
|
+
↓
|
|
833
|
+
[Usuário: "E a entrega?"]
|
|
834
|
+
↓
|
|
835
|
+
[Knowledge] → continua no mesmo agente
|
|
836
|
+
↓
|
|
837
|
+
[Usuário: "Quero falar com humano"]
|
|
838
|
+
↓
|
|
839
|
+
[Knowledge] → handoff para Escalation
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Configuração Recomendada
|
|
843
|
+
|
|
844
|
+
Para a maioria dos casos de uso:
|
|
845
|
+
|
|
846
|
+
```yaml
|
|
847
|
+
global: false
|
|
848
|
+
|
|
849
|
+
agents:
|
|
850
|
+
knowledge: true # FAQ
|
|
851
|
+
answer: true # Perguntas gerais
|
|
852
|
+
confirmation: true # Confirmações
|
|
853
|
+
|
|
854
|
+
interview: false # Coleta de dados
|
|
855
|
+
onboarding: false # Guia de usuário
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
860
|
+
## 🔐 Filtros de Acesso (Role/User)
|
|
861
|
+
|
|
862
|
+
O sistema de **Filtros de Acesso** permite controlar quais agentes, prompts e tools estão disponíveis para cada usuário ou role (função).
|
|
863
|
+
|
|
864
|
+
📂 **Exemplos completos**: [docs/examples/access_filters/](docs/examples/access_filters/)
|
|
865
|
+
|
|
866
|
+
### Quando Usar
|
|
867
|
+
|
|
868
|
+
| Cenário | Solução |
|
|
869
|
+
|---------|---------|
|
|
870
|
+
| **Multi-tenant** | Diferentes clientes veem diferentes agentes |
|
|
871
|
+
| **Níveis de acesso** | Admin vê mais opções que cliente |
|
|
872
|
+
| **Segurança** | Dados sensíveis só para roles específicas |
|
|
873
|
+
| **Personalização** | Diferentes instruções por departamento |
|
|
874
|
+
|
|
875
|
+
### Níveis de Filtragem
|
|
876
|
+
|
|
877
|
+
1. **Agentes**: Habilitar/desabilitar agentes inteiros
|
|
878
|
+
2. **Prompts**: Adicionar seções condicionais aos prompts
|
|
879
|
+
3. **Tools**: Habilitar/desabilitar tools específicas
|
|
880
|
+
|
|
881
|
+
### Exemplo 1: Filtros de Agente (Via Código)
|
|
882
|
+
|
|
883
|
+
```python
|
|
884
|
+
from pathlib import Path
|
|
885
|
+
from atendentepro import (
|
|
886
|
+
create_standard_network,
|
|
887
|
+
UserContext,
|
|
888
|
+
AccessFilter,
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# Usuário com role de vendedor
|
|
892
|
+
user = UserContext(user_id="vendedor_123", role="vendedor")
|
|
893
|
+
|
|
894
|
+
# Filtros de agente
|
|
895
|
+
agent_filters = {
|
|
896
|
+
# Feedback só para admin
|
|
897
|
+
"feedback": AccessFilter(allowed_roles=["admin"]),
|
|
898
|
+
# Escalation para todos exceto clientes
|
|
899
|
+
"escalation": AccessFilter(denied_roles=["cliente"]),
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
network = create_standard_network(
|
|
903
|
+
templates_root=Path("./meu_cliente"),
|
|
904
|
+
client="config",
|
|
905
|
+
user_context=user,
|
|
906
|
+
agent_filters=agent_filters,
|
|
907
|
+
)
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
### Exemplo 2: Prompts Condicionais
|
|
911
|
+
|
|
912
|
+
Adicione instruções específicas baseadas na role:
|
|
913
|
+
|
|
914
|
+
```python
|
|
915
|
+
from atendentepro import FilteredPromptSection
|
|
916
|
+
|
|
917
|
+
conditional_prompts = {
|
|
918
|
+
"knowledge": [
|
|
919
|
+
# Seção para vendedores
|
|
920
|
+
FilteredPromptSection(
|
|
921
|
+
content="\\n## Descontos\\nVocê pode oferecer até 15% de desconto.",
|
|
922
|
+
filter=AccessFilter(allowed_roles=["vendedor"]),
|
|
923
|
+
),
|
|
924
|
+
# Seção para admin
|
|
925
|
+
FilteredPromptSection(
|
|
926
|
+
content="\\n## Admin\\nVocê tem acesso total ao sistema.",
|
|
927
|
+
filter=AccessFilter(allowed_roles=["admin"]),
|
|
928
|
+
),
|
|
929
|
+
],
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
network = create_standard_network(
|
|
933
|
+
templates_root=Path("./meu_cliente"),
|
|
934
|
+
client="config",
|
|
935
|
+
user_context=user,
|
|
936
|
+
conditional_prompts=conditional_prompts,
|
|
937
|
+
)
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### Exemplo 3: Tools Filtradas
|
|
941
|
+
|
|
942
|
+
```python
|
|
943
|
+
from atendentepro import FilteredTool
|
|
944
|
+
from agents import function_tool
|
|
945
|
+
|
|
946
|
+
@function_tool
|
|
947
|
+
def deletar_cliente(cliente_id: str) -> str:
|
|
948
|
+
"""Remove um cliente do sistema."""
|
|
949
|
+
return f"Cliente {cliente_id} removido"
|
|
950
|
+
|
|
951
|
+
filtered_tools = {
|
|
952
|
+
"knowledge": [
|
|
953
|
+
FilteredTool(
|
|
954
|
+
tool=deletar_cliente,
|
|
955
|
+
filter=AccessFilter(allowed_roles=["admin"]), # Só admin
|
|
956
|
+
),
|
|
957
|
+
],
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
network = create_standard_network(
|
|
961
|
+
templates_root=Path("./meu_cliente"),
|
|
962
|
+
client="config",
|
|
963
|
+
user_context=user,
|
|
964
|
+
filtered_tools=filtered_tools,
|
|
965
|
+
)
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
### Via YAML (access_config.yaml)
|
|
969
|
+
|
|
970
|
+
```yaml
|
|
971
|
+
# Filtros de agente
|
|
972
|
+
agent_filters:
|
|
973
|
+
feedback:
|
|
974
|
+
allowed_roles: ["admin"]
|
|
975
|
+
escalation:
|
|
976
|
+
denied_roles: ["cliente"]
|
|
977
|
+
|
|
978
|
+
# Prompts condicionais
|
|
979
|
+
conditional_prompts:
|
|
980
|
+
knowledge:
|
|
981
|
+
- content: |
|
|
982
|
+
## Capacidades de Vendedor
|
|
983
|
+
Você pode oferecer até 15% de desconto.
|
|
984
|
+
filter:
|
|
985
|
+
allowed_roles: ["vendedor"]
|
|
986
|
+
|
|
987
|
+
# Acesso a tools
|
|
988
|
+
tool_access:
|
|
989
|
+
deletar_cliente:
|
|
990
|
+
allowed_roles: ["admin"]
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
### Tipos de Filtro
|
|
994
|
+
|
|
995
|
+
| Tipo | Descrição | Exemplo |
|
|
996
|
+
|------|-----------|---------|
|
|
997
|
+
| `allowed_roles` | Whitelist de roles | `["admin", "gerente"]` |
|
|
998
|
+
| `denied_roles` | Blacklist de roles | `["cliente"]` |
|
|
999
|
+
| `allowed_users` | Whitelist de usuários | `["user_vip_1"]` |
|
|
1000
|
+
| `denied_users` | Blacklist de usuários | `["user_bloqueado"]` |
|
|
1001
|
+
|
|
1002
|
+
### Prioridade de Avaliação
|
|
1003
|
+
|
|
1004
|
+
1. `denied_users` - Se usuário está negado, **bloqueia**
|
|
1005
|
+
2. `allowed_users` - Se lista existe e usuário está nela, **permite**
|
|
1006
|
+
3. `denied_roles` - Se role está negada, **bloqueia**
|
|
1007
|
+
4. `allowed_roles` - Se lista existe e role não está nela, **bloqueia**
|
|
1008
|
+
5. **Permite por padrão** - Se nenhum filtro matched
|
|
780
1009
|
|
|
781
|
-
|
|
1010
|
+
### Fluxo Visual
|
|
782
1011
|
|
|
783
1012
|
```
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1013
|
+
┌────────────────────────────────────────────────┐
|
|
1014
|
+
│ Requisição: role="vendedor" │
|
|
1015
|
+
└────────────────────────────────────────────────┘
|
|
1016
|
+
│
|
|
1017
|
+
▼
|
|
1018
|
+
┌────────────────────────────────────────────────┐
|
|
1019
|
+
│ FILTRO DE AGENTES │
|
|
1020
|
+
│ Knowledge: ✅ (vendedor allowed) │
|
|
1021
|
+
│ Escalation: ✅ (vendedor not denied) │
|
|
1022
|
+
│ Feedback: ❌ (only admin) │
|
|
1023
|
+
└────────────────────────────────────────────────┘
|
|
1024
|
+
│
|
|
1025
|
+
▼
|
|
1026
|
+
┌────────────────────────────────────────────────┐
|
|
1027
|
+
│ FILTRO DE PROMPTS │
|
|
1028
|
+
│ Knowledge recebe: "## Descontos..." │
|
|
1029
|
+
│ (seção condicional para vendedor) │
|
|
1030
|
+
└────────────────────────────────────────────────┘
|
|
1031
|
+
│
|
|
1032
|
+
▼
|
|
1033
|
+
┌────────────────────────────────────────────────┐
|
|
1034
|
+
│ FILTRO DE TOOLS │
|
|
1035
|
+
│ consultar_comissao: ✅ │
|
|
1036
|
+
│ deletar_cliente: ❌ (only admin) │
|
|
1037
|
+
└────────────────────────────────────────────────┘
|
|
789
1038
|
```
|
|
790
1039
|
|
|
791
1040
|
---
|
|
@@ -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=ZIN7Nrrx04b5mq4YovL3wk6O6oTY659uffZOI9rYNbE,5820
|
|
3
3
|
atendentepro/license.py,sha256=rlPtysXNqAzEQkP2VjUAVu_nMndhPgfKv1yN2ruUYVI,17570
|
|
4
|
-
atendentepro/network.py,sha256=
|
|
4
|
+
atendentepro/network.py,sha256=RmNrdf-78qWjlzxB7B8Awa-fJ7ZPQi5iQHZuHBPgVpo,28900
|
|
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
|
|
@@ -17,8 +17,8 @@ atendentepro/config/__init__.py,sha256=LQZbEz9AVGnO8xCG_rI9GmyFQun5HQqo8hJkkm-d_
|
|
|
17
17
|
atendentepro/config/settings.py,sha256=Z1texEtO76Rrt-wPyEXM3FyujDE75r8HbEEJClzFOvk,4762
|
|
18
18
|
atendentepro/guardrails/__init__.py,sha256=lCSI4RbQeIOya8k1wIADoxz9gYySDtUtOv-JiXisvyQ,454
|
|
19
19
|
atendentepro/guardrails/manager.py,sha256=KeyQkIfyPYuHIbuCPP-N4y2R3AJ6n2thkEO_avDgY7M,14053
|
|
20
|
-
atendentepro/models/__init__.py,sha256=
|
|
21
|
-
atendentepro/models/context.py,sha256=
|
|
20
|
+
atendentepro/models/__init__.py,sha256=H0l-WKYAW60hiU4uyVtteWRnH7l-8_WbVq-j5VqTNXM,638
|
|
21
|
+
atendentepro/models/context.py,sha256=M_5RSsiT0C5C4D_NtLfUYj9mPRtqk4Wdn5SiwH91uXg,5931
|
|
22
22
|
atendentepro/models/outputs.py,sha256=B573co699DhK0TSpqOHUykUOnBVa2Ool4rnNEO-xmUA,3270
|
|
23
23
|
atendentepro/prompts/__init__.py,sha256=pks7i00GXGM9J3UnZyCn4dDVLb4C8xSJ_yWAvpK3OM4,1390
|
|
24
24
|
atendentepro/prompts/answer.py,sha256=0VBKRzOXXlxg0IfIq1Et0XlDpX3akKASNb9NY_pesuM,3916
|
|
@@ -30,14 +30,14 @@ atendentepro/prompts/interview.py,sha256=9zVGA8zSmm6pBx2i1LPGNJIPuLlz7PZKRJE_PC6
|
|
|
30
30
|
atendentepro/prompts/knowledge.py,sha256=B3BOyAvzQlwAkR51gc-B6XbQLtwIZlwGP1ofhZ4cFbk,4644
|
|
31
31
|
atendentepro/prompts/onboarding.py,sha256=78fSIh2ifsGeoav8DV41_jnyU157c0dtggJujcDvW4U,6093
|
|
32
32
|
atendentepro/prompts/triage.py,sha256=bSdEVheGy03r5P6MQuv7NwhN2_wrt0mK80F9f_LskRU,1283
|
|
33
|
-
atendentepro/templates/__init__.py,sha256=
|
|
34
|
-
atendentepro/templates/manager.py,sha256=
|
|
33
|
+
atendentepro/templates/__init__.py,sha256=zV1CP2K7_WD219NXl-daTC3Iq8P9sQ7XLmxPEVI2NZg,1575
|
|
34
|
+
atendentepro/templates/manager.py,sha256=s2ezeyEboeMxdcb6oOADQRAm0ikB8Ru4fYC87gfctU0,28819
|
|
35
35
|
atendentepro/utils/__init__.py,sha256=WCJ6_btsLaI6xxHXvNHNue-nKrXWTKscNZGTToQiJ8A,833
|
|
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.
|
|
39
|
-
atendentepro-0.6.
|
|
40
|
-
atendentepro-0.6.
|
|
41
|
-
atendentepro-0.6.
|
|
42
|
-
atendentepro-0.6.
|
|
43
|
-
atendentepro-0.6.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|