atendentepro 0.3.0__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/README.md +890 -0
- atendentepro/__init__.py +215 -0
- atendentepro/agents/__init__.py +45 -0
- atendentepro/agents/answer.py +62 -0
- atendentepro/agents/confirmation.py +69 -0
- atendentepro/agents/flow.py +64 -0
- atendentepro/agents/interview.py +68 -0
- atendentepro/agents/knowledge.py +296 -0
- atendentepro/agents/onboarding.py +65 -0
- atendentepro/agents/triage.py +57 -0
- atendentepro/agents/usage.py +56 -0
- atendentepro/config/__init__.py +19 -0
- atendentepro/config/settings.py +134 -0
- atendentepro/guardrails/__init__.py +21 -0
- atendentepro/guardrails/manager.py +419 -0
- atendentepro/license.py +502 -0
- atendentepro/models/__init__.py +21 -0
- atendentepro/models/context.py +21 -0
- atendentepro/models/outputs.py +118 -0
- atendentepro/network.py +325 -0
- atendentepro/prompts/__init__.py +35 -0
- atendentepro/prompts/answer.py +114 -0
- atendentepro/prompts/confirmation.py +124 -0
- atendentepro/prompts/flow.py +112 -0
- atendentepro/prompts/interview.py +123 -0
- atendentepro/prompts/knowledge.py +135 -0
- atendentepro/prompts/onboarding.py +146 -0
- atendentepro/prompts/triage.py +42 -0
- atendentepro/templates/__init__.py +51 -0
- atendentepro/templates/manager.py +530 -0
- atendentepro/utils/__init__.py +19 -0
- atendentepro/utils/openai_client.py +154 -0
- atendentepro/utils/tracing.py +71 -0
- atendentepro-0.3.0.dist-info/METADATA +306 -0
- atendentepro-0.3.0.dist-info/RECORD +39 -0
- atendentepro-0.3.0.dist-info/WHEEL +5 -0
- atendentepro-0.3.0.dist-info/entry_points.txt +2 -0
- atendentepro-0.3.0.dist-info/licenses/LICENSE +25 -0
- atendentepro-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Guardrails Manager for AtendentePro.
|
|
4
|
+
|
|
5
|
+
Provides scope validation and content filtering for agents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import textwrap
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from agents import (
|
|
20
|
+
Agent,
|
|
21
|
+
GuardrailFunctionOutput,
|
|
22
|
+
RunContextWrapper,
|
|
23
|
+
Runner,
|
|
24
|
+
TResponseInputItem,
|
|
25
|
+
input_guardrail,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from atendentepro.models import GuardrailValidationOutput
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Type alias for guardrail callables
|
|
34
|
+
GuardrailCallable = Callable[
|
|
35
|
+
[RunContextWrapper[None], Agent, "Union[str, List[TResponseInputItem]]"],
|
|
36
|
+
Awaitable[GuardrailFunctionOutput],
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Cache of created guardrails by agent
|
|
41
|
+
_GUARDRAIL_CACHE: Dict[str, List[GuardrailCallable]] = {}
|
|
42
|
+
|
|
43
|
+
# Control of dynamically loaded template
|
|
44
|
+
_GUARDRAIL_TEMPLATE_ROOT: Optional[Path] = None
|
|
45
|
+
_GUARDRAIL_TEMPLATE_NAME: Optional[str] = None
|
|
46
|
+
_DEFAULT_TEMPLATE_FALLBACKS: tuple[str, ...] = ("standard",)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Examples of out-of-scope questions
|
|
50
|
+
OUT_OF_SCOPE_EXAMPLES = [
|
|
51
|
+
"Qual é o preço do bitcoin?",
|
|
52
|
+
"Distância entre a Terra e a Lua.",
|
|
53
|
+
"Resolva a equação 2x + 5 = 11.",
|
|
54
|
+
"Como programar uma API em Python?",
|
|
55
|
+
"Conte uma piada sobre futebol.",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class GuardrailManager:
|
|
60
|
+
"""
|
|
61
|
+
Manages guardrail configuration and creation for agents.
|
|
62
|
+
|
|
63
|
+
This class provides a high-level interface for loading guardrail
|
|
64
|
+
configurations from YAML files and creating guardrail functions
|
|
65
|
+
for agents.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
templates_root: Optional[Path] = None,
|
|
71
|
+
template_name: Optional[str] = None,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Initialize the GuardrailManager.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
templates_root: Root directory for template configurations.
|
|
78
|
+
template_name: Name of the template to use.
|
|
79
|
+
"""
|
|
80
|
+
self.templates_root = templates_root
|
|
81
|
+
self.template_name = template_name
|
|
82
|
+
self._cache: Dict[str, List[GuardrailCallable]] = {}
|
|
83
|
+
|
|
84
|
+
def load_config(self) -> Dict[str, Any]:
|
|
85
|
+
"""Load guardrail configuration from the configured template."""
|
|
86
|
+
return load_guardrail_config(
|
|
87
|
+
templates_root=self.templates_root,
|
|
88
|
+
template_name=self.template_name,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def get_guardrails(self, agent_name: str) -> List[GuardrailCallable]:
|
|
92
|
+
"""Get guardrails for a specific agent."""
|
|
93
|
+
if agent_name in self._cache:
|
|
94
|
+
return self._cache[agent_name]
|
|
95
|
+
|
|
96
|
+
guardrails = get_guardrails_for_agent(
|
|
97
|
+
agent_name,
|
|
98
|
+
templates_root=self.templates_root,
|
|
99
|
+
template_name=self.template_name,
|
|
100
|
+
)
|
|
101
|
+
self._cache[agent_name] = guardrails
|
|
102
|
+
return guardrails
|
|
103
|
+
|
|
104
|
+
def clear_cache(self) -> None:
|
|
105
|
+
"""Clear the guardrail cache."""
|
|
106
|
+
self._cache.clear()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@lru_cache(maxsize=1)
|
|
110
|
+
def _load_guardrail_config_cached(
|
|
111
|
+
templates_root: Optional[str] = None,
|
|
112
|
+
template_name: Optional[str] = None,
|
|
113
|
+
) -> Dict[str, Any]:
|
|
114
|
+
"""Internal cached config loader."""
|
|
115
|
+
root = Path(templates_root) if templates_root else _GUARDRAIL_TEMPLATE_ROOT
|
|
116
|
+
name = template_name or _GUARDRAIL_TEMPLATE_NAME
|
|
117
|
+
|
|
118
|
+
candidate_paths: List[Path] = []
|
|
119
|
+
|
|
120
|
+
if root and name:
|
|
121
|
+
candidate_paths.append(root / name / "guardrails_config.yaml")
|
|
122
|
+
|
|
123
|
+
if root:
|
|
124
|
+
for folder in _DEFAULT_TEMPLATE_FALLBACKS:
|
|
125
|
+
candidate_paths.append(root / folder / "guardrails_config.yaml")
|
|
126
|
+
|
|
127
|
+
# Deduplicate paths
|
|
128
|
+
unique_candidates: List[Path] = []
|
|
129
|
+
seen: set[Path] = set()
|
|
130
|
+
for path in candidate_paths:
|
|
131
|
+
resolved = path.resolve()
|
|
132
|
+
if resolved in seen:
|
|
133
|
+
continue
|
|
134
|
+
seen.add(resolved)
|
|
135
|
+
unique_candidates.append(path)
|
|
136
|
+
|
|
137
|
+
for path in unique_candidates:
|
|
138
|
+
if path.exists():
|
|
139
|
+
with path.open("r", encoding="utf-8") as file:
|
|
140
|
+
data = yaml.safe_load(file) or {}
|
|
141
|
+
return data if isinstance(data, dict) else {}
|
|
142
|
+
|
|
143
|
+
return {}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def load_guardrail_config(
|
|
147
|
+
templates_root: Optional[Path] = None,
|
|
148
|
+
template_name: Optional[str] = None,
|
|
149
|
+
) -> Dict[str, Any]:
|
|
150
|
+
"""
|
|
151
|
+
Load guardrail configuration from templates.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
templates_root: Root directory for templates.
|
|
155
|
+
template_name: Name of the template to load.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Dictionary containing the guardrail configuration.
|
|
159
|
+
"""
|
|
160
|
+
root_str = str(templates_root) if templates_root else None
|
|
161
|
+
return _load_guardrail_config_cached(root_str, template_name)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _normalize_agent_name(agent_name: str) -> str:
|
|
165
|
+
"""Normalize agent name for configuration lookup."""
|
|
166
|
+
name = agent_name.strip().lower().replace("-", " ")
|
|
167
|
+
return "_".join(name.split())
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _get_scope_for_agent(
|
|
171
|
+
agent_name: str,
|
|
172
|
+
templates_root: Optional[Path] = None,
|
|
173
|
+
template_name: Optional[str] = None,
|
|
174
|
+
) -> Optional[str]:
|
|
175
|
+
"""Retrieve the 'about' block for an agent from configuration."""
|
|
176
|
+
config = load_guardrail_config(templates_root, template_name)
|
|
177
|
+
scopes = config.get("agent_scopes", {})
|
|
178
|
+
normalized_name = _normalize_agent_name(agent_name)
|
|
179
|
+
|
|
180
|
+
if normalized_name in scopes:
|
|
181
|
+
about = scopes[normalized_name].get("about", "")
|
|
182
|
+
return about.strip() or None
|
|
183
|
+
|
|
184
|
+
# Fallback: try fuzzy matching
|
|
185
|
+
for key, value in scopes.items():
|
|
186
|
+
if _normalize_agent_name(key) == normalized_name:
|
|
187
|
+
about = value.get("about", "")
|
|
188
|
+
return about.strip() or None
|
|
189
|
+
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_out_of_scope_message(
|
|
194
|
+
agent_name: str,
|
|
195
|
+
templates_root: Optional[Path] = None,
|
|
196
|
+
template_name: Optional[str] = None,
|
|
197
|
+
) -> str:
|
|
198
|
+
"""
|
|
199
|
+
Get the appropriate 'out of scope' message for an agent.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
agent_name: Name of the agent.
|
|
203
|
+
templates_root: Root directory for templates.
|
|
204
|
+
template_name: Name of the template.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
The out of scope message for the agent.
|
|
208
|
+
"""
|
|
209
|
+
config = load_guardrail_config(templates_root, template_name)
|
|
210
|
+
out_of_scope_messages = config.get("out_of_scope_message", {})
|
|
211
|
+
normalized_name = _normalize_agent_name(agent_name)
|
|
212
|
+
|
|
213
|
+
# Try to find agent-specific message
|
|
214
|
+
if normalized_name in out_of_scope_messages:
|
|
215
|
+
message = out_of_scope_messages[normalized_name]
|
|
216
|
+
if isinstance(message, str) and message.strip():
|
|
217
|
+
return message.strip()
|
|
218
|
+
|
|
219
|
+
# Fall back to default message
|
|
220
|
+
default_message = out_of_scope_messages.get("default", "")
|
|
221
|
+
if isinstance(default_message, str) and default_message.strip():
|
|
222
|
+
return default_message.strip()
|
|
223
|
+
|
|
224
|
+
# Final fallback
|
|
225
|
+
return (
|
|
226
|
+
"Desculpe, mas a pergunta que você fez está fora do escopo dos tópicos que posso abordar. "
|
|
227
|
+
"Por favor, reformule sua pergunta para que ela se enquadre nos assuntos que estou autorizado a responder."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _build_guardrail_agent(agent_name: str, scope_description: str) -> Agent:
|
|
232
|
+
"""Create the agent responsible for scope validation."""
|
|
233
|
+
negative_examples = "\n".join(
|
|
234
|
+
f'- Pergunta: "{example}" -> is_in_scope: false'
|
|
235
|
+
for example in OUT_OF_SCOPE_EXAMPLES
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
instructions = f"""
|
|
239
|
+
Você é um agente de validação que decide se uma solicitação está dentro do escopo do {agent_name}.
|
|
240
|
+
|
|
241
|
+
CONTEXTO DO {agent_name.upper()}:
|
|
242
|
+
{scope_description.strip()}
|
|
243
|
+
|
|
244
|
+
REGRAS:
|
|
245
|
+
1. Use apenas o contexto fornecido. Não invente ou busque informações externas.
|
|
246
|
+
2. Considere mensagens neutras de saudação, agradecimento, confirmação ou pedidos para continuar a conversa como dentro do escopo (retorne is_in_scope: true).
|
|
247
|
+
3. Retorne is_in_scope: false apenas quando a mensagem tratar claramente de assuntos que não têm relação com o escopo acima.
|
|
248
|
+
4. Perguntas sobre assuntos gerais, entretenimento, ciências exatas, programação ou temas aleatórios devem ser marcadas como is_in_scope: false.
|
|
249
|
+
5. Responda somente com JSON compatível com o formato {{"is_in_scope": bool, "reasoning": string}} e explique brevemente a decisão.
|
|
250
|
+
|
|
251
|
+
EXEMPLOS FORA DO ESCOPO:
|
|
252
|
+
{negative_examples}
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
instructions = textwrap.dedent(instructions).strip()
|
|
256
|
+
|
|
257
|
+
return Agent(
|
|
258
|
+
name=f"{agent_name} Guardrail",
|
|
259
|
+
instructions=instructions,
|
|
260
|
+
output_type=GuardrailValidationOutput,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _create_guardrail_callable(
|
|
265
|
+
agent_name: str,
|
|
266
|
+
guardrail_agent: Agent,
|
|
267
|
+
) -> GuardrailCallable:
|
|
268
|
+
"""Build the decorated guardrail function for a specific agent."""
|
|
269
|
+
|
|
270
|
+
def _as_prompt(value: Union[str, List[TResponseInputItem]]) -> str:
|
|
271
|
+
if isinstance(value, str):
|
|
272
|
+
return value
|
|
273
|
+
|
|
274
|
+
collected: List[str] = []
|
|
275
|
+
for item in value:
|
|
276
|
+
if isinstance(item, dict):
|
|
277
|
+
content = item.get("content")
|
|
278
|
+
if isinstance(content, str):
|
|
279
|
+
collected.append(content.strip())
|
|
280
|
+
elif isinstance(content, list):
|
|
281
|
+
for part in content:
|
|
282
|
+
if isinstance(part, dict):
|
|
283
|
+
text_val = part.get("text")
|
|
284
|
+
if isinstance(text_val, str) and text_val.strip():
|
|
285
|
+
collected.append(text_val.strip())
|
|
286
|
+
elif isinstance(text_val, dict):
|
|
287
|
+
inner_value = text_val.get("value")
|
|
288
|
+
if isinstance(inner_value, str) and inner_value.strip():
|
|
289
|
+
collected.append(inner_value.strip())
|
|
290
|
+
elif isinstance(part, str) and part.strip():
|
|
291
|
+
collected.append(part.strip())
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
text_attr = getattr(item, "input_text", None)
|
|
295
|
+
if isinstance(text_attr, str) and text_attr.strip():
|
|
296
|
+
collected.append(text_attr.strip())
|
|
297
|
+
|
|
298
|
+
content_attr = getattr(item, "content", None)
|
|
299
|
+
if isinstance(content_attr, str):
|
|
300
|
+
collected.append(content_attr.strip())
|
|
301
|
+
elif isinstance(content_attr, list):
|
|
302
|
+
for part in content_attr:
|
|
303
|
+
text_part = getattr(part, "text", None)
|
|
304
|
+
if not text_part:
|
|
305
|
+
continue
|
|
306
|
+
part_value = getattr(text_part, "value", None)
|
|
307
|
+
if isinstance(part_value, str) and part_value.strip():
|
|
308
|
+
collected.append(part_value.strip())
|
|
309
|
+
elif isinstance(text_part, str) and text_part.strip():
|
|
310
|
+
collected.append(text_part.strip())
|
|
311
|
+
|
|
312
|
+
if collected:
|
|
313
|
+
return "\n".join(collected)
|
|
314
|
+
|
|
315
|
+
return str(value)
|
|
316
|
+
|
|
317
|
+
@input_guardrail
|
|
318
|
+
async def guardrail(
|
|
319
|
+
ctx: RunContextWrapper[None],
|
|
320
|
+
agent: Agent,
|
|
321
|
+
input: Union[str, List[TResponseInputItem]],
|
|
322
|
+
) -> GuardrailFunctionOutput:
|
|
323
|
+
prompt = _as_prompt(input)
|
|
324
|
+
|
|
325
|
+
result = await Runner.run(guardrail_agent, prompt, context=ctx.context)
|
|
326
|
+
output = result.final_output
|
|
327
|
+
|
|
328
|
+
if output is None:
|
|
329
|
+
return GuardrailFunctionOutput(
|
|
330
|
+
output_info=None,
|
|
331
|
+
tripwire_triggered=True,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
is_in_scope = bool(getattr(output, "is_in_scope", False))
|
|
335
|
+
return GuardrailFunctionOutput(
|
|
336
|
+
output_info=output,
|
|
337
|
+
tripwire_triggered=not is_in_scope,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
guardrail.__name__ = f"{_normalize_agent_name(agent_name)}_guardrail"
|
|
341
|
+
return guardrail
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_guardrails_for_agent(
|
|
345
|
+
agent_name: str,
|
|
346
|
+
templates_root: Optional[Path] = None,
|
|
347
|
+
template_name: Optional[str] = None,
|
|
348
|
+
) -> List[GuardrailCallable]:
|
|
349
|
+
"""
|
|
350
|
+
Get the list of guardrails configured for an agent.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
agent_name: Name of the agent (e.g., "Triage Agent", "Flow Agent").
|
|
354
|
+
templates_root: Root directory for templates.
|
|
355
|
+
template_name: Name of the template.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
List of guardrail callables for the agent.
|
|
359
|
+
"""
|
|
360
|
+
cache_key = f"{agent_name}:{templates_root}:{template_name}"
|
|
361
|
+
|
|
362
|
+
if cache_key in _GUARDRAIL_CACHE:
|
|
363
|
+
return _GUARDRAIL_CACHE[cache_key]
|
|
364
|
+
|
|
365
|
+
scope = _get_scope_for_agent(agent_name, templates_root, template_name)
|
|
366
|
+
if not scope:
|
|
367
|
+
_GUARDRAIL_CACHE[cache_key] = []
|
|
368
|
+
return []
|
|
369
|
+
|
|
370
|
+
guardrail_agent = _build_guardrail_agent(agent_name, scope)
|
|
371
|
+
guardrail_callable = _create_guardrail_callable(agent_name, guardrail_agent)
|
|
372
|
+
_GUARDRAIL_CACHE[cache_key] = [guardrail_callable]
|
|
373
|
+
return _GUARDRAIL_CACHE[cache_key]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def set_guardrails_client(
|
|
377
|
+
client_key: str,
|
|
378
|
+
*,
|
|
379
|
+
template_name: Optional[str] = None,
|
|
380
|
+
templates_root: Optional[Path] = None,
|
|
381
|
+
) -> Dict[str, Any]:
|
|
382
|
+
"""
|
|
383
|
+
Update the active guardrails template and clear associated caches.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
client_key: Client identifier.
|
|
387
|
+
template_name: Name of the template folder.
|
|
388
|
+
templates_root: Root directory for templates.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
The loaded configuration dictionary.
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
FileNotFoundError: If the guardrail config file doesn't exist.
|
|
395
|
+
"""
|
|
396
|
+
root = Path(templates_root) if templates_root else None
|
|
397
|
+
template_folder = template_name or client_key
|
|
398
|
+
|
|
399
|
+
if root:
|
|
400
|
+
config_path = root / template_folder / "guardrails_config.yaml"
|
|
401
|
+
if not config_path.exists():
|
|
402
|
+
raise FileNotFoundError(
|
|
403
|
+
f"Guardrail config not found for client '{client_key}' at path '{config_path}'"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
global _GUARDRAIL_TEMPLATE_ROOT, _GUARDRAIL_TEMPLATE_NAME
|
|
407
|
+
_GUARDRAIL_TEMPLATE_ROOT = root
|
|
408
|
+
_GUARDRAIL_TEMPLATE_NAME = template_folder
|
|
409
|
+
|
|
410
|
+
clear_guardrail_cache()
|
|
411
|
+
|
|
412
|
+
return load_guardrail_config(root, template_folder)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def clear_guardrail_cache() -> None:
|
|
416
|
+
"""Clear all guardrail caches."""
|
|
417
|
+
_GUARDRAIL_CACHE.clear()
|
|
418
|
+
_load_guardrail_config_cached.cache_clear()
|
|
419
|
+
|