atendentepro 0.6.3__py3-none-any.whl → 0.6.5__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
@@ -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",
@@ -1,7 +1,13 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """Models module for AtendentePro library."""
3
3
 
4
- from .context import ContextNote
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",
@@ -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 minimal network (only Triage, Flow, Interview, Answer)
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
- include_knowledge=False,
264
- include_confirmation=False,
265
- include_usage=False,
266
- include_escalation=False,
267
- include_feedback=False,
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=get_style_for_agent("triage"),
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=get_style_for_agent("flow"),
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=get_style_for_agent("interview"),
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=get_style_for_agent("answer"),
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=get_style_for_agent("knowledge"),
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=get_style_for_agent("confirmation"),
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=get_style_for_agent("usage"),
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=get_style_for_agent("escalation"),
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=get_style_for_agent("feedback"),
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=get_style_for_agent("onboarding"),
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
3
+ Version: 0.6.5
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,8 @@ 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)
99
+ - [Múltiplos Agentes](#-múltiplos-agentes-multi-interview--knowledge)
98
100
  - [Tracing e Monitoramento](#-tracing-e-monitoramento)
99
101
  - [Suporte](#-suporte)
100
102
 
@@ -712,82 +714,466 @@ agents:
712
714
 
713
715
  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
716
 
717
+ 📂 **Exemplos completos**: [docs/examples/single_reply/](docs/examples/single_reply/)
718
+
715
719
  ### Quando Usar
716
720
 
717
- - **Chatbots de alto volume**: Respostas rápidas e independentes
718
- - **Consultas simples**: Uma pergunta = uma resposta
719
- - **Evitar loops**: Impedir que usuários fiquem presos em um agente
721
+ | Cenário | Recomendação |
722
+ |---------|--------------|
723
+ | **Chatbots de alto volume** | Ativar para respostas rápidas |
724
+ | **FAQ simples** | ✅ Knowledge com single_reply |
725
+ | **Coleta de dados** | ❌ Interview precisa múltiplas interações |
726
+ | **Onboarding** | ❌ Precisa guiar o usuário em etapas |
727
+ | **Confirmações** | ✅ Confirma e volta ao Triage |
720
728
 
721
- ### Via Código
729
+ ### Exemplo 1: FAQ Bot (Via Código)
730
+
731
+ Chatbot otimizado para perguntas frequentes:
722
732
 
723
733
  ```python
724
734
  from pathlib import Path
725
735
  from atendentepro import create_standard_network
726
736
 
727
- # Ativar para TODOS os agentes
737
+ # FAQ Bot: Knowledge e Answer respondem uma vez
728
738
  network = create_standard_network(
729
739
  templates_root=Path("./meu_cliente"),
730
740
  client="config",
731
- global_single_reply=True, # Todos respondem uma vez
741
+ global_single_reply=False,
742
+ single_reply_agents={
743
+ "knowledge": True, # FAQ: responde e volta
744
+ "answer": True, # Perguntas gerais: responde e volta
745
+ "flow": True, # Menu: apresenta e volta
746
+ },
732
747
  )
748
+ ```
749
+
750
+ ### Exemplo 2: Bot de Leads (Via Código)
733
751
 
734
- # Ativar apenas para agentes específicos
752
+ Bot que coleta dados mas responde dúvidas rapidamente:
753
+
754
+ ```python
735
755
  network = create_standard_network(
736
756
  templates_root=Path("./meu_cliente"),
737
757
  client="config",
738
758
  global_single_reply=False,
739
759
  single_reply_agents={
740
- "knowledge": True, # Knowledge responde uma vez
741
- "confirmation": True, # Confirmation responde uma vez
742
- "answer": True, # Answer responde uma vez
743
- # interview: False # Interview pode ter múltiplas interações
760
+ # Interview PRECISA de múltiplas interações para coletar dados
761
+ "interview": False,
762
+
763
+ # Outros agentes podem ser rápidos
764
+ "knowledge": True, # Tira dúvidas sobre produto
765
+ "answer": True, # Responde perguntas
766
+ "confirmation": True, # Confirma cadastro
744
767
  },
745
768
  )
746
769
  ```
747
770
 
771
+ ### Exemplo 3: Ativar para TODOS os agentes
772
+
773
+ ```python
774
+ network = create_standard_network(
775
+ templates_root=Path("./meu_cliente"),
776
+ client="config",
777
+ global_single_reply=True, # Todos respondem uma vez
778
+ )
779
+ ```
780
+
748
781
  ### Via YAML (single_reply_config.yaml)
749
782
 
750
783
  Crie o arquivo `single_reply_config.yaml` na pasta do cliente:
751
784
 
752
785
  ```yaml
753
- # Global: aplica a todos os agentes
786
+ # Global: se true, TODOS os agentes respondem apenas uma vez
787
+ global: false
788
+
789
+ # Configuração por agente (sobrescreve global)
790
+ agents:
791
+ # Agentes de consulta: respondem uma vez
792
+ knowledge: true # FAQ: responde e volta
793
+ answer: true # Perguntas: responde e volta
794
+ confirmation: true # Confirma e volta
795
+ usage: true # Explica uso e volta
796
+
797
+ # Agentes de coleta: múltiplas interações
798
+ interview: false # Precisa coletar dados
799
+ onboarding: false # Precisa guiar usuário
800
+
801
+ # Opcionais
802
+ flow: true # Menu: apresenta e volta
803
+ escalation: true # Registra e volta
804
+ feedback: true # Coleta feedback e volta
805
+ ```
806
+
807
+ ### Fluxo Visual
808
+
809
+ **Com single_reply=True:**
810
+
811
+ ```
812
+ [Usuário: "Qual o preço?"]
813
+
814
+ [Triage] → detecta consulta
815
+
816
+ [Knowledge] → responde: "R$ 99,90"
817
+
818
+ [Triage] ← retorno AUTOMÁTICO
819
+
820
+ [Usuário: "E a entrega?"]
821
+
822
+ [Triage] → nova análise (ciclo reinicia)
823
+ ```
824
+
825
+ **Com single_reply=False (padrão):**
826
+
827
+ ```
828
+ [Usuário: "Qual o preço?"]
829
+
830
+ [Triage] → detecta consulta
831
+
832
+ [Knowledge] → responde: "R$ 99,90"
833
+
834
+ [Usuário: "E a entrega?"]
835
+
836
+ [Knowledge] → continua no mesmo agente
837
+
838
+ [Usuário: "Quero falar com humano"]
839
+
840
+ [Knowledge] → handoff para Escalation
841
+ ```
842
+
843
+ ### Configuração Recomendada
844
+
845
+ Para a maioria dos casos de uso:
846
+
847
+ ```yaml
754
848
  global: false
755
849
 
756
- # Configuração por agente
757
850
  agents:
758
- # Agentes que respondem uma vez
759
- knowledge: true
760
- confirmation: true
761
- answer: true
851
+ knowledge: true # FAQ
852
+ answer: true # Perguntas gerais
853
+ confirmation: true # Confirmações
762
854
 
763
- # Agentes que precisam de múltiplas interações
764
- interview: false
765
- flow: false
766
- onboarding: false
855
+ interview: false # Coleta de dados
856
+ onboarding: false # Guia de usuário
857
+ ```
858
+
859
+ ---
860
+
861
+ ## 🔐 Filtros de Acesso (Role/User)
862
+
863
+ 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).
864
+
865
+ 📂 **Exemplos completos**: [docs/examples/access_filters/](docs/examples/access_filters/)
866
+
867
+ ### Quando Usar
868
+
869
+ | Cenário | Solução |
870
+ |---------|---------|
871
+ | **Multi-tenant** | Diferentes clientes veem diferentes agentes |
872
+ | **Níveis de acesso** | Admin vê mais opções que cliente |
873
+ | **Segurança** | Dados sensíveis só para roles específicas |
874
+ | **Personalização** | Diferentes instruções por departamento |
875
+
876
+ ### Níveis de Filtragem
877
+
878
+ 1. **Agentes**: Habilitar/desabilitar agentes inteiros
879
+ 2. **Prompts**: Adicionar seções condicionais aos prompts
880
+ 3. **Tools**: Habilitar/desabilitar tools específicas
881
+
882
+ ### Exemplo 1: Filtros de Agente (Via Código)
883
+
884
+ ```python
885
+ from pathlib import Path
886
+ from atendentepro import (
887
+ create_standard_network,
888
+ UserContext,
889
+ AccessFilter,
890
+ )
891
+
892
+ # Usuário com role de vendedor
893
+ user = UserContext(user_id="vendedor_123", role="vendedor")
894
+
895
+ # Filtros de agente
896
+ agent_filters = {
897
+ # Feedback só para admin
898
+ "feedback": AccessFilter(allowed_roles=["admin"]),
899
+ # Escalation para todos exceto clientes
900
+ "escalation": AccessFilter(denied_roles=["cliente"]),
901
+ }
902
+
903
+ network = create_standard_network(
904
+ templates_root=Path("./meu_cliente"),
905
+ client="config",
906
+ user_context=user,
907
+ agent_filters=agent_filters,
908
+ )
909
+ ```
910
+
911
+ ### Exemplo 2: Prompts Condicionais
912
+
913
+ Adicione instruções específicas baseadas na role:
914
+
915
+ ```python
916
+ from atendentepro import FilteredPromptSection
917
+
918
+ conditional_prompts = {
919
+ "knowledge": [
920
+ # Seção para vendedores
921
+ FilteredPromptSection(
922
+ content="\\n## Descontos\\nVocê pode oferecer até 15% de desconto.",
923
+ filter=AccessFilter(allowed_roles=["vendedor"]),
924
+ ),
925
+ # Seção para admin
926
+ FilteredPromptSection(
927
+ content="\\n## Admin\\nVocê tem acesso total ao sistema.",
928
+ filter=AccessFilter(allowed_roles=["admin"]),
929
+ ),
930
+ ],
931
+ }
932
+
933
+ network = create_standard_network(
934
+ templates_root=Path("./meu_cliente"),
935
+ client="config",
936
+ user_context=user,
937
+ conditional_prompts=conditional_prompts,
938
+ )
939
+ ```
940
+
941
+ ### Exemplo 3: Tools Filtradas
942
+
943
+ ```python
944
+ from atendentepro import FilteredTool
945
+ from agents import function_tool
946
+
947
+ @function_tool
948
+ def deletar_cliente(cliente_id: str) -> str:
949
+ """Remove um cliente do sistema."""
950
+ return f"Cliente {cliente_id} removido"
951
+
952
+ filtered_tools = {
953
+ "knowledge": [
954
+ FilteredTool(
955
+ tool=deletar_cliente,
956
+ filter=AccessFilter(allowed_roles=["admin"]), # Só admin
957
+ ),
958
+ ],
959
+ }
960
+
961
+ network = create_standard_network(
962
+ templates_root=Path("./meu_cliente"),
963
+ client="config",
964
+ user_context=user,
965
+ filtered_tools=filtered_tools,
966
+ )
967
+ ```
968
+
969
+ ### Via YAML (access_config.yaml)
970
+
971
+ ```yaml
972
+ # Filtros de agente
973
+ agent_filters:
974
+ feedback:
975
+ allowed_roles: ["admin"]
976
+ escalation:
977
+ denied_roles: ["cliente"]
978
+
979
+ # Prompts condicionais
980
+ conditional_prompts:
981
+ knowledge:
982
+ - content: |
983
+ ## Capacidades de Vendedor
984
+ Você pode oferecer até 15% de desconto.
985
+ filter:
986
+ allowed_roles: ["vendedor"]
987
+
988
+ # Acesso a tools
989
+ tool_access:
990
+ deletar_cliente:
991
+ allowed_roles: ["admin"]
992
+ ```
993
+
994
+ ### Tipos de Filtro
995
+
996
+ | Tipo | Descrição | Exemplo |
997
+ |------|-----------|---------|
998
+ | `allowed_roles` | Whitelist de roles | `["admin", "gerente"]` |
999
+ | `denied_roles` | Blacklist de roles | `["cliente"]` |
1000
+ | `allowed_users` | Whitelist de usuários | `["user_vip_1"]` |
1001
+ | `denied_users` | Blacklist de usuários | `["user_bloqueado"]` |
1002
+
1003
+ ### Prioridade de Avaliação
1004
+
1005
+ 1. `denied_users` - Se usuário está negado, **bloqueia**
1006
+ 2. `allowed_users` - Se lista existe e usuário está nela, **permite**
1007
+ 3. `denied_roles` - Se role está negada, **bloqueia**
1008
+ 4. `allowed_roles` - Se lista existe e role não está nela, **bloqueia**
1009
+ 5. **Permite por padrão** - Se nenhum filtro matched
1010
+
1011
+ ### Fluxo Visual
1012
+
1013
+ ```
1014
+ ┌────────────────────────────────────────────────┐
1015
+ │ Requisição: role="vendedor" │
1016
+ └────────────────────────────────────────────────┘
1017
+
1018
+
1019
+ ┌────────────────────────────────────────────────┐
1020
+ │ FILTRO DE AGENTES │
1021
+ │ Knowledge: ✅ (vendedor allowed) │
1022
+ │ Escalation: ✅ (vendedor not denied) │
1023
+ │ Feedback: ❌ (only admin) │
1024
+ └────────────────────────────────────────────────┘
1025
+
1026
+
1027
+ ┌────────────────────────────────────────────────┐
1028
+ │ FILTRO DE PROMPTS │
1029
+ │ Knowledge recebe: "## Descontos..." │
1030
+ │ (seção condicional para vendedor) │
1031
+ └────────────────────────────────────────────────┘
1032
+
1033
+
1034
+ ┌────────────────────────────────────────────────┐
1035
+ │ FILTRO DE TOOLS │
1036
+ │ consultar_comissao: ✅ │
1037
+ │ deletar_cliente: ❌ (only admin) │
1038
+ └────────────────────────────────────────────────┘
767
1039
  ```
768
1040
 
769
- ### Comportamento
1041
+ ---
1042
+
1043
+ ## 🔀 Múltiplos Agentes (Multi Interview + Knowledge)
1044
+
1045
+ O AtendentePro suporta criar **múltiplas instâncias** de Interview e Knowledge agents, cada um especializado em um domínio diferente.
1046
+
1047
+ 📂 **Exemplo completo**: [docs/examples/multi_agents/](docs/examples/multi_agents/)
1048
+
1049
+ ### Caso de Uso
770
1050
 
771
- Quando `single_reply=True`:
1051
+ Empresa que atende diferentes tipos de clientes:
1052
+ - **Pessoa Física (PF)**: Produtos de consumo
1053
+ - **Pessoa Jurídica (PJ)**: Soluções empresariais
772
1054
 
1055
+ ### Arquitetura
1056
+
1057
+ ```
1058
+ ┌─────────────────┐
1059
+ │ Triage │
1060
+ │ (entry point) │
1061
+ └────────┬────────┘
1062
+
1063
+ ┌──────────────┼──────────────┐
1064
+ │ │ │
1065
+ ▼ ▼ ▼
1066
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
1067
+ │ Interview │ │ Interview │ │ Flow │
1068
+ │ PF │ │ PJ │ │ (comum) │
1069
+ └──────┬──────┘ └──────┬──────┘ └─────────────┘
1070
+ │ │
1071
+ ▼ ▼
1072
+ ┌─────────────┐ ┌─────────────┐
1073
+ │ Knowledge │ │ Knowledge │
1074
+ │ PF │ │ PJ │
1075
+ └─────────────┘ └─────────────┘
773
1076
  ```
774
- [Usuário] → [Triage] → [Knowledge]
775
-
776
- (responde uma vez)
777
-
778
- [Triage] (retorno automático)
1077
+
1078
+ ### Implementação
1079
+
1080
+ ```python
1081
+ from atendentepro import (
1082
+ create_custom_network,
1083
+ create_triage_agent,
1084
+ create_interview_agent,
1085
+ create_knowledge_agent,
1086
+ )
1087
+
1088
+ # 1. Criar agentes especializados
1089
+ interview_pf = create_interview_agent(
1090
+ interview_questions="CPF, data de nascimento, renda mensal",
1091
+ name="interview_pf", # Nome único!
1092
+ )
1093
+
1094
+ interview_pj = create_interview_agent(
1095
+ interview_questions="CNPJ, razão social, faturamento",
1096
+ name="interview_pj", # Nome único!
1097
+ )
1098
+
1099
+ knowledge_pf = create_knowledge_agent(
1100
+ knowledge_about="Produtos para consumidor final",
1101
+ name="knowledge_pf",
1102
+ single_reply=True,
1103
+ )
1104
+
1105
+ knowledge_pj = create_knowledge_agent(
1106
+ knowledge_about="Soluções empresariais B2B",
1107
+ name="knowledge_pj",
1108
+ single_reply=True,
1109
+ )
1110
+
1111
+ # 2. Criar Triage
1112
+ triage = create_triage_agent(
1113
+ keywords_text="PF: CPF, pessoal, minha conta | PJ: CNPJ, empresa, MEI",
1114
+ name="triage_agent",
1115
+ )
1116
+
1117
+ # 3. Configurar handoffs
1118
+ triage.handoffs = [interview_pf, interview_pj, knowledge_pf, knowledge_pj]
1119
+ interview_pf.handoffs = [knowledge_pf, triage]
1120
+ interview_pj.handoffs = [knowledge_pj, triage]
1121
+ knowledge_pf.handoffs = [triage]
1122
+ knowledge_pj.handoffs = [triage]
1123
+
1124
+ # 4. Criar network customizada
1125
+ network = create_custom_network(
1126
+ triage=triage,
1127
+ custom_agents={
1128
+ "interview_pf": interview_pf,
1129
+ "interview_pj": interview_pj,
1130
+ "knowledge_pf": knowledge_pf,
1131
+ "knowledge_pj": knowledge_pj,
1132
+ },
1133
+ )
779
1134
  ```
780
1135
 
781
- Quando `single_reply=False` (padrão):
1136
+ ### Cenários de Roteamento
1137
+
1138
+ | Mensagem do Usuário | Rota |
1139
+ |---------------------|------|
1140
+ | "Quero abrir conta para mim" | Triage → Interview PF → Knowledge PF |
1141
+ | "Preciso de maquininha para minha loja" | Triage → Interview PJ → Knowledge PJ |
1142
+ | "Quanto custa o cartão gold?" | Triage → Knowledge PF (direto) |
1143
+ | "Capital de giro para empresa" | Triage → Knowledge PJ (direto) |
1144
+
1145
+ ### Padrão: 1 Interview → 2 Knowledge
1146
+
1147
+ Outro padrão comum é ter um único Interview que pode direcionar para múltiplos Knowledge:
782
1148
 
783
1149
  ```
784
- [Usuário] → [Triage] → [Knowledge]
785
-
786
- (pode continuar)
787
-
788
- [Usuário]
1150
+ ┌───────────────┐
1151
+ │ Interview │
1152
+ (coleta dados)
1153
+ └───────┬───────┘
1154
+
1155
+ ┌───────┴───────┐
1156
+ ▼ ▼
1157
+ ┌───────────────┐ ┌───────────────┐
1158
+ │ Knowledge │ │ Knowledge │
1159
+ │ Produtos │ │Troubleshooting│
1160
+ └───────────────┘ └───────────────┘
1161
+ ```
1162
+
1163
+ ```python
1164
+ # Um interview que direciona para múltiplos knowledge
1165
+ interview.handoffs = [knowledge_produtos, knowledge_troubleshooting, triage]
789
1166
  ```
790
1167
 
1168
+ 📂 **Exemplo completo**: [example_one_interview_two_knowledge.py](docs/examples/multi_agents/example_one_interview_two_knowledge.py)
1169
+
1170
+ ### Dicas
1171
+
1172
+ 1. **Nomes únicos**: Cada agente precisa de um `name` distinto
1173
+ 2. **Handoffs claros**: Configure quais agentes cada um pode chamar
1174
+ 3. **Keywords no Triage**: Inclua palavras-chave para direcionar corretamente
1175
+ 4. **single_reply**: Use em Knowledge para evitar loops
1176
+
791
1177
  ---
792
1178
 
793
1179
  ## 📊 Tracing e Monitoramento
@@ -1,7 +1,7 @@
1
1
  atendentepro/README.md,sha256=TAXl5GRjhSwz_I-Dx_eN5JOIcUxuE_dz31iMZ_-OnRY,45390
2
- atendentepro/__init__.py,sha256=CDK61bxE18NYvLeswIoQWLarw2_xJAOEb2yONsQ2CqQ,5606
2
+ atendentepro/__init__.py,sha256=ZIN7Nrrx04b5mq4YovL3wk6O6oTY659uffZOI9rYNbE,5820
3
3
  atendentepro/license.py,sha256=rlPtysXNqAzEQkP2VjUAVu_nMndhPgfKv1yN2ruUYVI,17570
4
- atendentepro/network.py,sha256=QEWgvNEWNkr1-xHd54dIdfFIgGJsRLBgQpuaDnQXknM,24124
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=eUMq0Lw21PGItGliKHxFDeETIqOVqEwkXVOjgJPM5I0,390
21
- atendentepro/models/context.py,sha256=1WsoHEtXodgfd5JyZDZctV_2uabNq_XAKv0HKmWb-G8,543
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=Xc3yTQcdUbzKphbOYEAefhV1v9FzLas_WzuphYVpk_0,1377
34
- atendentepro/templates/manager.py,sha256=VHnu2dxWA6TR07QjJtsDZIRkwIjq-zGoIDysul7WMkE,23347
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.3.dist-info/licenses/LICENSE,sha256=TF6CdXxePoT9DXtPnCejiU5mUwWzrFzd1iyWJyoMauA,983
39
- atendentepro-0.6.3.dist-info/METADATA,sha256=ijYMZnbksuX5O7ao9bA0deK6gI5eYM3jtE2S_hd2WOE,27835
40
- atendentepro-0.6.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
- atendentepro-0.6.3.dist-info/entry_points.txt,sha256=OP0upzqJF3MLS6VX-M-5BfUwx5YLJO2sJ3YBAp4e6yI,89
42
- atendentepro-0.6.3.dist-info/top_level.txt,sha256=BFasD4SMmgDUmWKlTIZ1PeuukoRBhyiMIz8umKWVCcs,13
43
- atendentepro-0.6.3.dist-info/RECORD,,
38
+ atendentepro-0.6.5.dist-info/licenses/LICENSE,sha256=TF6CdXxePoT9DXtPnCejiU5mUwWzrFzd1iyWJyoMauA,983
39
+ atendentepro-0.6.5.dist-info/METADATA,sha256=kLTh75smJ0t4HHAGVY7Bcj4XoOzKtrLWL_gcu1OvLEA,40666
40
+ atendentepro-0.6.5.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
41
+ atendentepro-0.6.5.dist-info/entry_points.txt,sha256=OP0upzqJF3MLS6VX-M-5BfUwx5YLJO2sJ3YBAp4e6yI,89
42
+ atendentepro-0.6.5.dist-info/top_level.txt,sha256=BFasD4SMmgDUmWKlTIZ1PeuukoRBhyiMIz8umKWVCcs,13
43
+ atendentepro-0.6.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5