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,530 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Template Manager for AtendentePro.
|
|
4
|
+
|
|
5
|
+
Provides dynamic client-specific configuration loading from external template directories.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable, Dict, List, Literal, Optional
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ClientTemplate:
|
|
21
|
+
"""Metadata required to load and configure a client profile."""
|
|
22
|
+
|
|
23
|
+
key: str
|
|
24
|
+
template_name: str
|
|
25
|
+
aliases: tuple[str, ...] = ()
|
|
26
|
+
configurator: Optional[Callable[[str], None]] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FlowTopic(BaseModel):
|
|
30
|
+
"""Model for a flow topic."""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
label: str
|
|
34
|
+
keywords: List[str] = Field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FlowConfig(BaseModel):
|
|
38
|
+
"""Configuration model for Flow Agent."""
|
|
39
|
+
|
|
40
|
+
topics: List[FlowTopic] = Field(default_factory=list)
|
|
41
|
+
keywords: List[Dict[str, Any]] = Field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
@lru_cache(maxsize=4)
|
|
45
|
+
def load(cls, path: Path) -> "FlowConfig":
|
|
46
|
+
"""Load flow configuration from YAML file."""
|
|
47
|
+
if not path.exists():
|
|
48
|
+
raise FileNotFoundError(f"Flow config not found at {path}")
|
|
49
|
+
|
|
50
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
51
|
+
data = yaml.safe_load(f) or {}
|
|
52
|
+
|
|
53
|
+
topics = []
|
|
54
|
+
for topic_data in data.get("topics", []):
|
|
55
|
+
topics.append(FlowTopic(
|
|
56
|
+
id=topic_data.get("id", ""),
|
|
57
|
+
label=topic_data.get("label", ""),
|
|
58
|
+
keywords=topic_data.get("keywords", []),
|
|
59
|
+
))
|
|
60
|
+
|
|
61
|
+
return cls(
|
|
62
|
+
topics=topics,
|
|
63
|
+
keywords=data.get("keywords", []),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def get_flow_template(self) -> str:
|
|
67
|
+
"""Generate formatted topic template."""
|
|
68
|
+
return "\n".join(
|
|
69
|
+
f"{idx}. {topic.label}"
|
|
70
|
+
for idx, topic in enumerate(self.topics, start=1)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def get_flow_keywords(self) -> str:
|
|
74
|
+
"""Generate formatted keywords text."""
|
|
75
|
+
lines = []
|
|
76
|
+
for topic in self.topics:
|
|
77
|
+
if topic.keywords:
|
|
78
|
+
formatted_terms = ", ".join(f'"{term}"' for term in topic.keywords)
|
|
79
|
+
lines.append(f"- {topic.label}: {formatted_terms}")
|
|
80
|
+
return "\n".join(lines)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class InterviewConfig(BaseModel):
|
|
84
|
+
"""Configuration model for Interview Agent."""
|
|
85
|
+
|
|
86
|
+
interview_questions: str = ""
|
|
87
|
+
topics: List[Dict[str, Any]] = Field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
@lru_cache(maxsize=4)
|
|
91
|
+
def load(cls, path: Path) -> "InterviewConfig":
|
|
92
|
+
"""Load interview configuration from YAML file."""
|
|
93
|
+
if not path.exists():
|
|
94
|
+
raise FileNotFoundError(f"Interview config not found at {path}")
|
|
95
|
+
|
|
96
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
97
|
+
data = yaml.safe_load(f) or {}
|
|
98
|
+
|
|
99
|
+
return cls(
|
|
100
|
+
interview_questions=data.get("interview_questions", ""),
|
|
101
|
+
topics=data.get("topics", []),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TriageConfig(BaseModel):
|
|
106
|
+
"""Configuration model for Triage Agent keywords."""
|
|
107
|
+
|
|
108
|
+
keywords: Dict[str, List[str]] = Field(default_factory=dict)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
@lru_cache(maxsize=4)
|
|
112
|
+
def load(cls, path: Path) -> "TriageConfig":
|
|
113
|
+
"""Load triage configuration from YAML file."""
|
|
114
|
+
if not path.exists():
|
|
115
|
+
raise FileNotFoundError(f"Triage config not found at {path}")
|
|
116
|
+
|
|
117
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
118
|
+
data = yaml.safe_load(f) or {}
|
|
119
|
+
|
|
120
|
+
keywords = {}
|
|
121
|
+
for agent, entry in data.get("keywords", {}).items():
|
|
122
|
+
if isinstance(entry, dict):
|
|
123
|
+
keywords[agent] = entry.get("keywords", [])
|
|
124
|
+
elif isinstance(entry, list):
|
|
125
|
+
keywords[agent] = entry
|
|
126
|
+
|
|
127
|
+
return cls(keywords=keywords)
|
|
128
|
+
|
|
129
|
+
def get_keywords_text(self) -> str:
|
|
130
|
+
"""Format keywords for agent instructions."""
|
|
131
|
+
lines = []
|
|
132
|
+
for agent, kws in self.keywords.items():
|
|
133
|
+
if kws:
|
|
134
|
+
formatted = ", ".join(f'"{kw}"' for kw in kws)
|
|
135
|
+
lines.append(f"- {agent}: {formatted}")
|
|
136
|
+
return "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DataSourceColumn(BaseModel):
|
|
140
|
+
"""Model for a data source column."""
|
|
141
|
+
|
|
142
|
+
name: str
|
|
143
|
+
description: str = ""
|
|
144
|
+
type: str = "string" # string, number, date, boolean
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class DataSourceConfig(BaseModel):
|
|
148
|
+
"""Configuration for structured data sources."""
|
|
149
|
+
|
|
150
|
+
type: str = "csv" # csv, database, api
|
|
151
|
+
path: Optional[str] = None # For CSV files
|
|
152
|
+
connection_env: Optional[str] = None # For database connections
|
|
153
|
+
api_url: Optional[str] = None # For API endpoints
|
|
154
|
+
encoding: str = "utf-8"
|
|
155
|
+
columns: List[DataSourceColumn] = Field(default_factory=list)
|
|
156
|
+
tables: List[Dict[str, str]] = Field(default_factory=list) # For databases
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class DocumentConfig(BaseModel):
|
|
160
|
+
"""Configuration for a knowledge document."""
|
|
161
|
+
|
|
162
|
+
name: str
|
|
163
|
+
path: str
|
|
164
|
+
description: str = ""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class KnowledgeConfig(BaseModel):
|
|
168
|
+
"""Configuration model for Knowledge Agent.
|
|
169
|
+
|
|
170
|
+
Supports both document-based RAG and structured data sources.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
about: str = ""
|
|
174
|
+
template: str = ""
|
|
175
|
+
format: str = ""
|
|
176
|
+
|
|
177
|
+
# Document-based RAG
|
|
178
|
+
embeddings_path: Optional[str] = None
|
|
179
|
+
documents: List[DocumentConfig] = Field(default_factory=list)
|
|
180
|
+
|
|
181
|
+
# Structured data sources
|
|
182
|
+
data_sources: List[DataSourceConfig] = Field(default_factory=list)
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
@lru_cache(maxsize=4)
|
|
186
|
+
def load(cls, path: Path) -> "KnowledgeConfig":
|
|
187
|
+
"""Load knowledge configuration from YAML file."""
|
|
188
|
+
if not path.exists():
|
|
189
|
+
raise FileNotFoundError(f"Knowledge config not found at {path}")
|
|
190
|
+
|
|
191
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
192
|
+
data = yaml.safe_load(f) or {}
|
|
193
|
+
|
|
194
|
+
# Parse documents
|
|
195
|
+
documents = []
|
|
196
|
+
for doc_data in data.get("documents", []):
|
|
197
|
+
documents.append(DocumentConfig(
|
|
198
|
+
name=doc_data.get("name", ""),
|
|
199
|
+
path=doc_data.get("path", ""),
|
|
200
|
+
description=doc_data.get("description", ""),
|
|
201
|
+
))
|
|
202
|
+
|
|
203
|
+
# Parse data sources
|
|
204
|
+
data_sources = []
|
|
205
|
+
|
|
206
|
+
# Check for single data_source (backwards compatibility)
|
|
207
|
+
if "data_source" in data:
|
|
208
|
+
ds = data["data_source"]
|
|
209
|
+
columns = [
|
|
210
|
+
DataSourceColumn(
|
|
211
|
+
name=c.get("name", ""),
|
|
212
|
+
description=c.get("description", ""),
|
|
213
|
+
type=c.get("type", "string"),
|
|
214
|
+
)
|
|
215
|
+
for c in ds.get("columns", [])
|
|
216
|
+
]
|
|
217
|
+
data_sources.append(DataSourceConfig(
|
|
218
|
+
type=ds.get("type", "csv"),
|
|
219
|
+
path=ds.get("path"),
|
|
220
|
+
connection_env=ds.get("connection_env"),
|
|
221
|
+
api_url=ds.get("api_url"),
|
|
222
|
+
encoding=ds.get("encoding", "utf-8"),
|
|
223
|
+
columns=columns,
|
|
224
|
+
tables=ds.get("tables", []),
|
|
225
|
+
))
|
|
226
|
+
|
|
227
|
+
# Check for multiple data_sources
|
|
228
|
+
for ds in data.get("data_sources", []):
|
|
229
|
+
columns = [
|
|
230
|
+
DataSourceColumn(
|
|
231
|
+
name=c.get("name", ""),
|
|
232
|
+
description=c.get("description", ""),
|
|
233
|
+
type=c.get("type", "string"),
|
|
234
|
+
)
|
|
235
|
+
for c in ds.get("columns", [])
|
|
236
|
+
]
|
|
237
|
+
data_sources.append(DataSourceConfig(
|
|
238
|
+
type=ds.get("type", "csv"),
|
|
239
|
+
path=ds.get("path"),
|
|
240
|
+
connection_env=ds.get("connection_env"),
|
|
241
|
+
api_url=ds.get("api_url"),
|
|
242
|
+
encoding=ds.get("encoding", "utf-8"),
|
|
243
|
+
columns=columns,
|
|
244
|
+
tables=ds.get("tables", []),
|
|
245
|
+
))
|
|
246
|
+
|
|
247
|
+
return cls(
|
|
248
|
+
about=data.get("about", ""),
|
|
249
|
+
template=data.get("template", ""),
|
|
250
|
+
format=data.get("format", ""),
|
|
251
|
+
embeddings_path=data.get("embeddings_path"),
|
|
252
|
+
documents=documents,
|
|
253
|
+
data_sources=data_sources,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def has_documents(self) -> bool:
|
|
257
|
+
"""Check if document-based RAG is configured."""
|
|
258
|
+
return bool(self.embeddings_path or self.documents)
|
|
259
|
+
|
|
260
|
+
def has_data_sources(self) -> bool:
|
|
261
|
+
"""Check if structured data sources are configured."""
|
|
262
|
+
return bool(self.data_sources)
|
|
263
|
+
|
|
264
|
+
def get_data_source_description(self) -> str:
|
|
265
|
+
"""Generate description of available data sources."""
|
|
266
|
+
parts = []
|
|
267
|
+
|
|
268
|
+
if self.documents:
|
|
269
|
+
doc_list = ", ".join(d.name for d in self.documents)
|
|
270
|
+
parts.append(f"Documentos: {doc_list}")
|
|
271
|
+
|
|
272
|
+
for ds in self.data_sources:
|
|
273
|
+
if ds.type == "csv":
|
|
274
|
+
cols = ", ".join(c.name for c in ds.columns)
|
|
275
|
+
parts.append(f"CSV ({ds.path}): {cols}")
|
|
276
|
+
elif ds.type == "database":
|
|
277
|
+
tables = ", ".join(t.get("name", "") for t in ds.tables)
|
|
278
|
+
parts.append(f"Database: {tables}")
|
|
279
|
+
elif ds.type == "api":
|
|
280
|
+
parts.append(f"API: {ds.api_url}")
|
|
281
|
+
|
|
282
|
+
return "\n".join(parts)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class ConfirmationConfig(BaseModel):
|
|
286
|
+
"""Configuration model for Confirmation Agent."""
|
|
287
|
+
|
|
288
|
+
about: str = ""
|
|
289
|
+
template: str = ""
|
|
290
|
+
format: str = ""
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
@lru_cache(maxsize=4)
|
|
294
|
+
def load(cls, path: Path) -> "ConfirmationConfig":
|
|
295
|
+
"""Load confirmation configuration from YAML file."""
|
|
296
|
+
if not path.exists():
|
|
297
|
+
raise FileNotFoundError(f"Confirmation config not found at {path}")
|
|
298
|
+
|
|
299
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
300
|
+
data = yaml.safe_load(f) or {}
|
|
301
|
+
|
|
302
|
+
return cls(
|
|
303
|
+
about=data.get("about", ""),
|
|
304
|
+
template=data.get("template", ""),
|
|
305
|
+
format=data.get("format", ""),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class OnboardingField(BaseModel):
|
|
310
|
+
"""Model for an onboarding required field."""
|
|
311
|
+
|
|
312
|
+
name: str
|
|
313
|
+
prompt: str
|
|
314
|
+
priority: int = 0
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class OnboardingConfig(BaseModel):
|
|
318
|
+
"""Configuration model for Onboarding Agent."""
|
|
319
|
+
|
|
320
|
+
required_fields: List[OnboardingField] = Field(default_factory=list)
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
@lru_cache(maxsize=4)
|
|
324
|
+
def load(cls, path: Path) -> "OnboardingConfig":
|
|
325
|
+
"""Load onboarding configuration from YAML file."""
|
|
326
|
+
if not path.exists():
|
|
327
|
+
raise FileNotFoundError(f"Onboarding config not found at {path}")
|
|
328
|
+
|
|
329
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
330
|
+
data = yaml.safe_load(f) or {}
|
|
331
|
+
|
|
332
|
+
fields = []
|
|
333
|
+
for field_data in data.get("required_fields", []):
|
|
334
|
+
fields.append(OnboardingField(
|
|
335
|
+
name=field_data.get("name", ""),
|
|
336
|
+
prompt=field_data.get("prompt", ""),
|
|
337
|
+
priority=field_data.get("priority", 0),
|
|
338
|
+
))
|
|
339
|
+
|
|
340
|
+
# Sort by priority
|
|
341
|
+
fields.sort(key=lambda x: x.priority)
|
|
342
|
+
|
|
343
|
+
return cls(required_fields=fields)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# Global template manager instance
|
|
347
|
+
_template_manager: Optional["TemplateManager"] = None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class TemplateManager:
|
|
351
|
+
"""
|
|
352
|
+
Coordinates dynamic client-specific configuration loading.
|
|
353
|
+
|
|
354
|
+
The manager handles loading configuration files from external template
|
|
355
|
+
directories and provides access to client-specific settings.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def __init__(
|
|
359
|
+
self,
|
|
360
|
+
templates_root: Optional[Path] = None,
|
|
361
|
+
default_client: str = "standard",
|
|
362
|
+
) -> None:
|
|
363
|
+
"""
|
|
364
|
+
Initialize the TemplateManager.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
templates_root: Root directory for template configurations.
|
|
368
|
+
default_client: Default client key to use.
|
|
369
|
+
"""
|
|
370
|
+
self.templates_root = templates_root
|
|
371
|
+
self.default_client = default_client
|
|
372
|
+
|
|
373
|
+
self._client_templates: Dict[str, ClientTemplate] = {}
|
|
374
|
+
self._client_aliases: Dict[str, str] = {}
|
|
375
|
+
self._configured_clients: Dict[str, bool] = {}
|
|
376
|
+
|
|
377
|
+
def register_client(self, template: ClientTemplate) -> None:
|
|
378
|
+
"""Register or update a client template."""
|
|
379
|
+
self._client_templates[template.key] = template
|
|
380
|
+
for alias in template.aliases:
|
|
381
|
+
normalized = alias.strip().lower()
|
|
382
|
+
if normalized:
|
|
383
|
+
self._client_aliases[normalized] = template.key
|
|
384
|
+
|
|
385
|
+
def normalize_client_key(self, client: Optional[str]) -> str:
|
|
386
|
+
"""Normalize client aliases to a canonical key."""
|
|
387
|
+
if not client:
|
|
388
|
+
return self.default_client
|
|
389
|
+
key = client.strip().lower()
|
|
390
|
+
return self._client_aliases.get(key, key)
|
|
391
|
+
|
|
392
|
+
def get_template_folder(self, client: Optional[str] = None) -> Path:
|
|
393
|
+
"""Return the Path for the requested client's template directory."""
|
|
394
|
+
if not self.templates_root:
|
|
395
|
+
raise ValueError("templates_root not configured")
|
|
396
|
+
|
|
397
|
+
client_key = self.normalize_client_key(client)
|
|
398
|
+
template = self._client_templates.get(client_key)
|
|
399
|
+
|
|
400
|
+
if template:
|
|
401
|
+
return self.templates_root / template.template_name
|
|
402
|
+
|
|
403
|
+
return self.templates_root / client_key
|
|
404
|
+
|
|
405
|
+
def load_flow_config(self, client: Optional[str] = None) -> FlowConfig:
|
|
406
|
+
"""Load flow configuration for the specified client."""
|
|
407
|
+
folder = self.get_template_folder(client)
|
|
408
|
+
return FlowConfig.load(folder / "flow_config.yaml")
|
|
409
|
+
|
|
410
|
+
def load_interview_config(self, client: Optional[str] = None) -> InterviewConfig:
|
|
411
|
+
"""Load interview configuration for the specified client."""
|
|
412
|
+
folder = self.get_template_folder(client)
|
|
413
|
+
return InterviewConfig.load(folder / "interview_config.yaml")
|
|
414
|
+
|
|
415
|
+
def load_triage_config(self, client: Optional[str] = None) -> TriageConfig:
|
|
416
|
+
"""Load triage configuration for the specified client."""
|
|
417
|
+
folder = self.get_template_folder(client)
|
|
418
|
+
return TriageConfig.load(folder / "triage_config.yaml")
|
|
419
|
+
|
|
420
|
+
def load_knowledge_config(self, client: Optional[str] = None) -> KnowledgeConfig:
|
|
421
|
+
"""Load knowledge configuration for the specified client."""
|
|
422
|
+
folder = self.get_template_folder(client)
|
|
423
|
+
return KnowledgeConfig.load(folder / "knowledge_config.yaml")
|
|
424
|
+
|
|
425
|
+
def load_confirmation_config(self, client: Optional[str] = None) -> ConfirmationConfig:
|
|
426
|
+
"""Load confirmation configuration for the specified client."""
|
|
427
|
+
folder = self.get_template_folder(client)
|
|
428
|
+
return ConfirmationConfig.load(folder / "confirmation_config.yaml")
|
|
429
|
+
|
|
430
|
+
def load_onboarding_config(self, client: Optional[str] = None) -> OnboardingConfig:
|
|
431
|
+
"""Load onboarding configuration for the specified client."""
|
|
432
|
+
folder = self.get_template_folder(client)
|
|
433
|
+
return OnboardingConfig.load(folder / "onboarding_config.yaml")
|
|
434
|
+
|
|
435
|
+
def clear_caches(self) -> None:
|
|
436
|
+
"""Clear all configuration caches."""
|
|
437
|
+
FlowConfig.load.cache_clear()
|
|
438
|
+
InterviewConfig.load.cache_clear()
|
|
439
|
+
TriageConfig.load.cache_clear()
|
|
440
|
+
KnowledgeConfig.load.cache_clear()
|
|
441
|
+
ConfirmationConfig.load.cache_clear()
|
|
442
|
+
OnboardingConfig.load.cache_clear()
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def get_template_manager() -> TemplateManager:
|
|
446
|
+
"""Get the global template manager instance."""
|
|
447
|
+
global _template_manager
|
|
448
|
+
if _template_manager is None:
|
|
449
|
+
_template_manager = TemplateManager()
|
|
450
|
+
return _template_manager
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def configure_template_manager(
|
|
454
|
+
templates_root: Path,
|
|
455
|
+
default_client: str = "standard",
|
|
456
|
+
) -> TemplateManager:
|
|
457
|
+
"""
|
|
458
|
+
Configure the global template manager.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
templates_root: Root directory for template configurations.
|
|
462
|
+
default_client: Default client key to use.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Configured TemplateManager instance.
|
|
466
|
+
"""
|
|
467
|
+
global _template_manager
|
|
468
|
+
_template_manager = TemplateManager(
|
|
469
|
+
templates_root=templates_root,
|
|
470
|
+
default_client=default_client,
|
|
471
|
+
)
|
|
472
|
+
return _template_manager
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def configure_client(
|
|
476
|
+
client: Optional[str] = None,
|
|
477
|
+
templates_root: Optional[Path] = None,
|
|
478
|
+
) -> str:
|
|
479
|
+
"""
|
|
480
|
+
Configure the template manager for a specific client.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
client: Client key or alias.
|
|
484
|
+
templates_root: Optional templates root to configure.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Normalized client key.
|
|
488
|
+
"""
|
|
489
|
+
manager = get_template_manager()
|
|
490
|
+
|
|
491
|
+
if templates_root:
|
|
492
|
+
manager.templates_root = templates_root
|
|
493
|
+
|
|
494
|
+
return manager.normalize_client_key(client)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def get_template_folder(client: Optional[str] = None) -> Path:
|
|
498
|
+
"""Get the template folder for the specified client."""
|
|
499
|
+
return get_template_manager().get_template_folder(client)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def load_flow_config(client: Optional[str] = None) -> FlowConfig:
|
|
503
|
+
"""Load flow configuration for the specified client."""
|
|
504
|
+
return get_template_manager().load_flow_config(client)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def load_interview_config(client: Optional[str] = None) -> InterviewConfig:
|
|
508
|
+
"""Load interview configuration for the specified client."""
|
|
509
|
+
return get_template_manager().load_interview_config(client)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def load_triage_config(client: Optional[str] = None) -> TriageConfig:
|
|
513
|
+
"""Load triage configuration for the specified client."""
|
|
514
|
+
return get_template_manager().load_triage_config(client)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def load_knowledge_config(client: Optional[str] = None) -> KnowledgeConfig:
|
|
518
|
+
"""Load knowledge configuration for the specified client."""
|
|
519
|
+
return get_template_manager().load_knowledge_config(client)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def load_confirmation_config(client: Optional[str] = None) -> ConfirmationConfig:
|
|
523
|
+
"""Load confirmation configuration for the specified client."""
|
|
524
|
+
return get_template_manager().load_confirmation_config(client)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def load_onboarding_config(client: Optional[str] = None) -> OnboardingConfig:
|
|
528
|
+
"""Load onboarding configuration for the specified client."""
|
|
529
|
+
return get_template_manager().load_onboarding_config(client)
|
|
530
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Utility modules for AtendentePro library."""
|
|
3
|
+
|
|
4
|
+
from .openai_client import (
|
|
5
|
+
get_async_client,
|
|
6
|
+
get_provider,
|
|
7
|
+
AsyncClient,
|
|
8
|
+
Provider,
|
|
9
|
+
)
|
|
10
|
+
from .tracing import configure_tracing
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"get_async_client",
|
|
14
|
+
"get_provider",
|
|
15
|
+
"AsyncClient",
|
|
16
|
+
"Provider",
|
|
17
|
+
"configure_tracing",
|
|
18
|
+
]
|
|
19
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
OpenAI Client utilities for AtendentePro.
|
|
4
|
+
|
|
5
|
+
Provides unified access to both OpenAI and Azure OpenAI clients.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from typing import Literal, Union
|
|
13
|
+
|
|
14
|
+
from atendentepro.config import get_config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _disable_azure_tracing() -> None:
|
|
18
|
+
"""Disable tracing for Azure OpenAI to avoid compatibility issues."""
|
|
19
|
+
_disable_flags = {
|
|
20
|
+
"OPENAI_TRACE_DISABLED": "true",
|
|
21
|
+
"OPENAI_TRACING_DISABLED": "true",
|
|
22
|
+
"OPENAI_TELEMETRY_DISABLED": "true",
|
|
23
|
+
"OPENAI_TRACE": "false",
|
|
24
|
+
"OPENAI_TELEMETRY": "false",
|
|
25
|
+
"OPENAI_TRACE_ENABLED": "false",
|
|
26
|
+
"OPENAI_TRACING_ENABLED": "false",
|
|
27
|
+
"OPENAI_TELEMETRY_ENABLED": "false",
|
|
28
|
+
"OPENAI_DISABLE_TRACING": "true",
|
|
29
|
+
"OPENAI_DISABLE_TELEMETRY": "true",
|
|
30
|
+
"AGENTS_TRACE_DISABLED": "true",
|
|
31
|
+
"OPENAI_AGENTS_TRACE_DISABLED": "true",
|
|
32
|
+
}
|
|
33
|
+
for key, value in _disable_flags.items():
|
|
34
|
+
os.environ.setdefault(key, value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Apply tracing disable for Azure if configured
|
|
38
|
+
config = get_config()
|
|
39
|
+
if config.provider == "azure":
|
|
40
|
+
_disable_azure_tracing()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
from openai import AsyncAzureOpenAI, AsyncOpenAI
|
|
44
|
+
|
|
45
|
+
# Try to disable tracing via SDK
|
|
46
|
+
try:
|
|
47
|
+
from openai import traces as _openai_traces
|
|
48
|
+
|
|
49
|
+
if hasattr(_openai_traces, "configure"):
|
|
50
|
+
_openai_traces.configure(enabled=False)
|
|
51
|
+
elif hasattr(_openai_traces, "set_enabled"):
|
|
52
|
+
_openai_traces.set_enabled(False)
|
|
53
|
+
elif hasattr(_openai_traces, "disable"):
|
|
54
|
+
_openai_traces.disable()
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
# Disable tracing modules
|
|
59
|
+
for _trace_module in (
|
|
60
|
+
"agents.tracing",
|
|
61
|
+
"agents.trace",
|
|
62
|
+
"openai.agents.tracing",
|
|
63
|
+
"openai.agents.trace",
|
|
64
|
+
):
|
|
65
|
+
try:
|
|
66
|
+
_mod = __import__(_trace_module, fromlist=["dummy"])
|
|
67
|
+
except Exception:
|
|
68
|
+
continue
|
|
69
|
+
for attr in ("set_tracing_client", "configure", "set_enabled", "disable"):
|
|
70
|
+
func = getattr(_mod, attr, None)
|
|
71
|
+
try:
|
|
72
|
+
if callable(func):
|
|
73
|
+
if attr == "set_tracing_client":
|
|
74
|
+
func(None)
|
|
75
|
+
elif attr == "configure":
|
|
76
|
+
func(enabled=False)
|
|
77
|
+
else:
|
|
78
|
+
func(False)
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
Provider = Literal["azure", "openai"]
|
|
84
|
+
AsyncClient = Union[AsyncAzureOpenAI, AsyncOpenAI]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_provider() -> Provider:
|
|
88
|
+
"""Return the configured provider."""
|
|
89
|
+
config = get_config()
|
|
90
|
+
if config.provider not in ("azure", "openai"):
|
|
91
|
+
return "openai"
|
|
92
|
+
return config.provider
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@lru_cache(maxsize=1)
|
|
96
|
+
def get_async_client() -> AsyncClient:
|
|
97
|
+
"""
|
|
98
|
+
Instantiate and cache the async OpenAI-compatible client.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
AsyncOpenAI or AsyncAzureOpenAI client based on configuration.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
RuntimeError: If required credentials are missing.
|
|
105
|
+
"""
|
|
106
|
+
config = get_config()
|
|
107
|
+
provider = get_provider()
|
|
108
|
+
|
|
109
|
+
if provider == "azure":
|
|
110
|
+
required = {
|
|
111
|
+
"AZURE_API_KEY": config.azure_api_key,
|
|
112
|
+
"AZURE_API_ENDPOINT": config.azure_api_endpoint,
|
|
113
|
+
"AZURE_API_VERSION": config.azure_api_version,
|
|
114
|
+
}
|
|
115
|
+
missing = [name for name, value in required.items() if not value]
|
|
116
|
+
|
|
117
|
+
if missing:
|
|
118
|
+
names = ", ".join(missing)
|
|
119
|
+
raise RuntimeError(f"Credenciais Azure OpenAI ausentes: {names}")
|
|
120
|
+
|
|
121
|
+
client = AsyncAzureOpenAI(
|
|
122
|
+
api_key=config.azure_api_key,
|
|
123
|
+
azure_endpoint=config.azure_api_endpoint,
|
|
124
|
+
api_version=config.azure_api_version,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Configure default deployment if provided
|
|
128
|
+
if config.azure_deployment_name:
|
|
129
|
+
client.azure_deployment = config.azure_deployment_name
|
|
130
|
+
|
|
131
|
+
# Best effort: disable tracing hooks on the instantiated client
|
|
132
|
+
try:
|
|
133
|
+
traces_attr = getattr(client, "traces", None)
|
|
134
|
+
if traces_attr is not None:
|
|
135
|
+
if hasattr(traces_attr, "disable"):
|
|
136
|
+
traces_attr.disable()
|
|
137
|
+
if hasattr(traces_attr, "set_enabled"):
|
|
138
|
+
traces_attr.set_enabled(False)
|
|
139
|
+
setattr(client, "traces", None)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
return client
|
|
144
|
+
|
|
145
|
+
if not config.openai_api_key:
|
|
146
|
+
raise RuntimeError("OPENAI_API_KEY não configurada. Defina-a ou selecione provider=azure.")
|
|
147
|
+
|
|
148
|
+
return AsyncOpenAI(api_key=config.openai_api_key)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def clear_client_cache() -> None:
|
|
152
|
+
"""Clear the cached client to allow reconfiguration."""
|
|
153
|
+
get_async_client.cache_clear()
|
|
154
|
+
|