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 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.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
- - **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
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
- # Ativar para TODOS os agentes
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=True, # Todos respondem uma vez
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
- # Ativar apenas para agentes específicos
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
- "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
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: aplica a todos os agentes
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 que respondem uma vez
759
- knowledge: true
760
- confirmation: true
761
- answer: true
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
- # Agentes que precisam de múltiplas interações
764
- interview: false
765
- flow: false
766
- onboarding: false
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
- ### Comportamento
806
+ ### Fluxo Visual
770
807
 
771
- Quando `single_reply=True`:
808
+ **Com single_reply=True:**
772
809
 
773
810
  ```
774
- [Usuário] [Triage] → [Knowledge]
775
-
776
- (responde uma vez)
777
-
778
- [Triage] (retorno automático)
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
- Quando `single_reply=False` (padrão):
1010
+ ### Fluxo Visual
782
1011
 
783
1012
  ```
784
- [Usuário] → [Triage] → [Knowledge]
785
-
786
- (pode continuar)
787
-
788
- [Usuário]
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=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.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,,