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.
Files changed (39) hide show
  1. atendentepro/README.md +890 -0
  2. atendentepro/__init__.py +215 -0
  3. atendentepro/agents/__init__.py +45 -0
  4. atendentepro/agents/answer.py +62 -0
  5. atendentepro/agents/confirmation.py +69 -0
  6. atendentepro/agents/flow.py +64 -0
  7. atendentepro/agents/interview.py +68 -0
  8. atendentepro/agents/knowledge.py +296 -0
  9. atendentepro/agents/onboarding.py +65 -0
  10. atendentepro/agents/triage.py +57 -0
  11. atendentepro/agents/usage.py +56 -0
  12. atendentepro/config/__init__.py +19 -0
  13. atendentepro/config/settings.py +134 -0
  14. atendentepro/guardrails/__init__.py +21 -0
  15. atendentepro/guardrails/manager.py +419 -0
  16. atendentepro/license.py +502 -0
  17. atendentepro/models/__init__.py +21 -0
  18. atendentepro/models/context.py +21 -0
  19. atendentepro/models/outputs.py +118 -0
  20. atendentepro/network.py +325 -0
  21. atendentepro/prompts/__init__.py +35 -0
  22. atendentepro/prompts/answer.py +114 -0
  23. atendentepro/prompts/confirmation.py +124 -0
  24. atendentepro/prompts/flow.py +112 -0
  25. atendentepro/prompts/interview.py +123 -0
  26. atendentepro/prompts/knowledge.py +135 -0
  27. atendentepro/prompts/onboarding.py +146 -0
  28. atendentepro/prompts/triage.py +42 -0
  29. atendentepro/templates/__init__.py +51 -0
  30. atendentepro/templates/manager.py +530 -0
  31. atendentepro/utils/__init__.py +19 -0
  32. atendentepro/utils/openai_client.py +154 -0
  33. atendentepro/utils/tracing.py +71 -0
  34. atendentepro-0.3.0.dist-info/METADATA +306 -0
  35. atendentepro-0.3.0.dist-info/RECORD +39 -0
  36. atendentepro-0.3.0.dist-info/WHEEL +5 -0
  37. atendentepro-0.3.0.dist-info/entry_points.txt +2 -0
  38. atendentepro-0.3.0.dist-info/licenses/LICENSE +25 -0
  39. 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
+