genxai-framework 0.1.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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +6 -0
- cli/commands/approval.py +85 -0
- cli/commands/audit.py +127 -0
- cli/commands/metrics.py +25 -0
- cli/commands/tool.py +389 -0
- cli/main.py +32 -0
- genxai/__init__.py +81 -0
- genxai/api/__init__.py +5 -0
- genxai/api/app.py +21 -0
- genxai/config/__init__.py +5 -0
- genxai/config/settings.py +37 -0
- genxai/connectors/__init__.py +19 -0
- genxai/connectors/base.py +122 -0
- genxai/connectors/kafka.py +92 -0
- genxai/connectors/postgres_cdc.py +95 -0
- genxai/connectors/registry.py +44 -0
- genxai/connectors/sqs.py +94 -0
- genxai/connectors/webhook.py +73 -0
- genxai/core/__init__.py +37 -0
- genxai/core/agent/__init__.py +32 -0
- genxai/core/agent/base.py +206 -0
- genxai/core/agent/config_io.py +59 -0
- genxai/core/agent/registry.py +98 -0
- genxai/core/agent/runtime.py +970 -0
- genxai/core/communication/__init__.py +6 -0
- genxai/core/communication/collaboration.py +44 -0
- genxai/core/communication/message_bus.py +192 -0
- genxai/core/communication/protocols.py +35 -0
- genxai/core/execution/__init__.py +22 -0
- genxai/core/execution/metadata.py +181 -0
- genxai/core/execution/queue.py +201 -0
- genxai/core/graph/__init__.py +30 -0
- genxai/core/graph/checkpoints.py +77 -0
- genxai/core/graph/edges.py +131 -0
- genxai/core/graph/engine.py +813 -0
- genxai/core/graph/executor.py +516 -0
- genxai/core/graph/nodes.py +161 -0
- genxai/core/graph/trigger_runner.py +40 -0
- genxai/core/memory/__init__.py +19 -0
- genxai/core/memory/base.py +72 -0
- genxai/core/memory/embedding.py +327 -0
- genxai/core/memory/episodic.py +448 -0
- genxai/core/memory/long_term.py +467 -0
- genxai/core/memory/manager.py +543 -0
- genxai/core/memory/persistence.py +297 -0
- genxai/core/memory/procedural.py +461 -0
- genxai/core/memory/semantic.py +526 -0
- genxai/core/memory/shared.py +62 -0
- genxai/core/memory/short_term.py +303 -0
- genxai/core/memory/vector_store.py +508 -0
- genxai/core/memory/working.py +211 -0
- genxai/core/state/__init__.py +6 -0
- genxai/core/state/manager.py +293 -0
- genxai/core/state/schema.py +115 -0
- genxai/llm/__init__.py +14 -0
- genxai/llm/base.py +150 -0
- genxai/llm/factory.py +329 -0
- genxai/llm/providers/__init__.py +1 -0
- genxai/llm/providers/anthropic.py +249 -0
- genxai/llm/providers/cohere.py +274 -0
- genxai/llm/providers/google.py +334 -0
- genxai/llm/providers/ollama.py +147 -0
- genxai/llm/providers/openai.py +257 -0
- genxai/llm/routing.py +83 -0
- genxai/observability/__init__.py +6 -0
- genxai/observability/logging.py +327 -0
- genxai/observability/metrics.py +494 -0
- genxai/observability/tracing.py +372 -0
- genxai/performance/__init__.py +39 -0
- genxai/performance/cache.py +256 -0
- genxai/performance/pooling.py +289 -0
- genxai/security/audit.py +304 -0
- genxai/security/auth.py +315 -0
- genxai/security/cost_control.py +528 -0
- genxai/security/default_policies.py +44 -0
- genxai/security/jwt.py +142 -0
- genxai/security/oauth.py +226 -0
- genxai/security/pii.py +366 -0
- genxai/security/policy_engine.py +82 -0
- genxai/security/rate_limit.py +341 -0
- genxai/security/rbac.py +247 -0
- genxai/security/validation.py +218 -0
- genxai/tools/__init__.py +21 -0
- genxai/tools/base.py +383 -0
- genxai/tools/builtin/__init__.py +131 -0
- genxai/tools/builtin/communication/__init__.py +15 -0
- genxai/tools/builtin/communication/email_sender.py +159 -0
- genxai/tools/builtin/communication/notification_manager.py +167 -0
- genxai/tools/builtin/communication/slack_notifier.py +118 -0
- genxai/tools/builtin/communication/sms_sender.py +118 -0
- genxai/tools/builtin/communication/webhook_caller.py +136 -0
- genxai/tools/builtin/computation/__init__.py +15 -0
- genxai/tools/builtin/computation/calculator.py +101 -0
- genxai/tools/builtin/computation/code_executor.py +183 -0
- genxai/tools/builtin/computation/data_validator.py +259 -0
- genxai/tools/builtin/computation/hash_generator.py +129 -0
- genxai/tools/builtin/computation/regex_matcher.py +201 -0
- genxai/tools/builtin/data/__init__.py +15 -0
- genxai/tools/builtin/data/csv_processor.py +213 -0
- genxai/tools/builtin/data/data_transformer.py +299 -0
- genxai/tools/builtin/data/json_processor.py +233 -0
- genxai/tools/builtin/data/text_analyzer.py +288 -0
- genxai/tools/builtin/data/xml_processor.py +175 -0
- genxai/tools/builtin/database/__init__.py +15 -0
- genxai/tools/builtin/database/database_inspector.py +157 -0
- genxai/tools/builtin/database/mongodb_query.py +196 -0
- genxai/tools/builtin/database/redis_cache.py +167 -0
- genxai/tools/builtin/database/sql_query.py +145 -0
- genxai/tools/builtin/database/vector_search.py +163 -0
- genxai/tools/builtin/file/__init__.py +17 -0
- genxai/tools/builtin/file/directory_scanner.py +214 -0
- genxai/tools/builtin/file/file_compressor.py +237 -0
- genxai/tools/builtin/file/file_reader.py +102 -0
- genxai/tools/builtin/file/file_writer.py +122 -0
- genxai/tools/builtin/file/image_processor.py +186 -0
- genxai/tools/builtin/file/pdf_parser.py +144 -0
- genxai/tools/builtin/test/__init__.py +15 -0
- genxai/tools/builtin/test/async_simulator.py +62 -0
- genxai/tools/builtin/test/data_transformer.py +99 -0
- genxai/tools/builtin/test/error_generator.py +82 -0
- genxai/tools/builtin/test/simple_math.py +94 -0
- genxai/tools/builtin/test/string_processor.py +72 -0
- genxai/tools/builtin/web/__init__.py +15 -0
- genxai/tools/builtin/web/api_caller.py +161 -0
- genxai/tools/builtin/web/html_parser.py +330 -0
- genxai/tools/builtin/web/http_client.py +187 -0
- genxai/tools/builtin/web/url_validator.py +162 -0
- genxai/tools/builtin/web/web_scraper.py +170 -0
- genxai/tools/custom/my_test_tool_2.py +9 -0
- genxai/tools/dynamic.py +105 -0
- genxai/tools/mcp_server.py +167 -0
- genxai/tools/persistence/__init__.py +6 -0
- genxai/tools/persistence/models.py +55 -0
- genxai/tools/persistence/service.py +322 -0
- genxai/tools/registry.py +227 -0
- genxai/tools/security/__init__.py +11 -0
- genxai/tools/security/limits.py +214 -0
- genxai/tools/security/policy.py +20 -0
- genxai/tools/security/sandbox.py +248 -0
- genxai/tools/templates.py +435 -0
- genxai/triggers/__init__.py +19 -0
- genxai/triggers/base.py +104 -0
- genxai/triggers/file_watcher.py +75 -0
- genxai/triggers/queue.py +68 -0
- genxai/triggers/registry.py +82 -0
- genxai/triggers/schedule.py +66 -0
- genxai/triggers/webhook.py +68 -0
- genxai/utils/__init__.py +1 -0
- genxai/utils/tokens.py +295 -0
- genxai_framework-0.1.0.dist-info/METADATA +495 -0
- genxai_framework-0.1.0.dist-info/RECORD +156 -0
- genxai_framework-0.1.0.dist-info/WHEEL +5 -0
- genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
- genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- genxai_framework-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""OpenTelemetry tracing for GenXAI."""
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, Callable, Dict, Optional
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from opentelemetry import trace
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
11
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
12
|
+
from opentelemetry.sdk.resources import Resource
|
|
13
|
+
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
|
|
14
|
+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
|
15
|
+
OTEL_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
OTEL_AVAILABLE = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TracingManager:
|
|
21
|
+
"""Manage OpenTelemetry tracing for GenXAI."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
service_name: str = "genxai",
|
|
26
|
+
jaeger_host: str = "localhost",
|
|
27
|
+
jaeger_port: int = 6831,
|
|
28
|
+
enable_console: bool = False
|
|
29
|
+
):
|
|
30
|
+
"""Initialize tracing manager.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
service_name: Name of the service
|
|
34
|
+
jaeger_host: Jaeger agent host
|
|
35
|
+
jaeger_port: Jaeger agent port
|
|
36
|
+
enable_console: Enable console exporter for debugging
|
|
37
|
+
"""
|
|
38
|
+
self.service_name = service_name
|
|
39
|
+
self.jaeger_host = jaeger_host
|
|
40
|
+
self.jaeger_port = jaeger_port
|
|
41
|
+
self.enable_console = enable_console
|
|
42
|
+
self.tracer = None
|
|
43
|
+
|
|
44
|
+
if OTEL_AVAILABLE:
|
|
45
|
+
self._setup_tracing()
|
|
46
|
+
|
|
47
|
+
def _setup_tracing(self):
|
|
48
|
+
"""Set up OpenTelemetry tracing."""
|
|
49
|
+
# Create resource
|
|
50
|
+
resource = Resource.create({
|
|
51
|
+
"service.name": self.service_name,
|
|
52
|
+
"service.version": "1.0.0",
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
# Create tracer provider
|
|
56
|
+
tracer_provider = TracerProvider(resource=resource)
|
|
57
|
+
|
|
58
|
+
# Add Jaeger exporter
|
|
59
|
+
try:
|
|
60
|
+
jaeger_exporter = JaegerExporter(
|
|
61
|
+
agent_host_name=self.jaeger_host,
|
|
62
|
+
agent_port=self.jaeger_port,
|
|
63
|
+
)
|
|
64
|
+
tracer_provider.add_span_processor(
|
|
65
|
+
BatchSpanProcessor(jaeger_exporter)
|
|
66
|
+
)
|
|
67
|
+
except Exception:
|
|
68
|
+
# Jaeger not available, continue without it
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# Add console exporter if enabled
|
|
72
|
+
if self.enable_console:
|
|
73
|
+
console_exporter = ConsoleSpanExporter()
|
|
74
|
+
tracer_provider.add_span_processor(
|
|
75
|
+
BatchSpanProcessor(console_exporter)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Set global tracer provider
|
|
79
|
+
trace.set_tracer_provider(tracer_provider)
|
|
80
|
+
|
|
81
|
+
# Get tracer
|
|
82
|
+
self.tracer = trace.get_tracer(__name__)
|
|
83
|
+
|
|
84
|
+
# Instrument requests library
|
|
85
|
+
try:
|
|
86
|
+
RequestsInstrumentor().instrument()
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def start_span(
|
|
91
|
+
self,
|
|
92
|
+
name: str,
|
|
93
|
+
attributes: Optional[Dict[str, Any]] = None
|
|
94
|
+
):
|
|
95
|
+
"""Start a new span.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
name: Span name
|
|
99
|
+
attributes: Optional span attributes
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Span context manager
|
|
103
|
+
"""
|
|
104
|
+
if not self.tracer:
|
|
105
|
+
return _NoOpSpan()
|
|
106
|
+
|
|
107
|
+
span = self.tracer.start_as_current_span(name)
|
|
108
|
+
|
|
109
|
+
if attributes:
|
|
110
|
+
for key, value in attributes.items():
|
|
111
|
+
span.set_attribute(key, value)
|
|
112
|
+
|
|
113
|
+
return span
|
|
114
|
+
|
|
115
|
+
@contextmanager
|
|
116
|
+
def span(
|
|
117
|
+
self,
|
|
118
|
+
name: str,
|
|
119
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
120
|
+
):
|
|
121
|
+
"""Context manager for a tracing span.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: Span name
|
|
125
|
+
attributes: Optional span attributes
|
|
126
|
+
"""
|
|
127
|
+
if not self.tracer:
|
|
128
|
+
yield _NoOpSpan()
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
with self.tracer.start_as_current_span(name) as span:
|
|
132
|
+
if attributes:
|
|
133
|
+
for key, value in attributes.items():
|
|
134
|
+
span.set_attribute(key, value)
|
|
135
|
+
yield span
|
|
136
|
+
|
|
137
|
+
def trace(
|
|
138
|
+
self,
|
|
139
|
+
span_name: Optional[str] = None,
|
|
140
|
+
attributes: Optional[Dict[str, Any]] = None
|
|
141
|
+
):
|
|
142
|
+
"""Decorator to trace a function.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
span_name: Optional span name (defaults to function name)
|
|
146
|
+
attributes: Optional span attributes
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Decorated function
|
|
150
|
+
"""
|
|
151
|
+
def decorator(func: Callable) -> Callable:
|
|
152
|
+
@wraps(func)
|
|
153
|
+
async def async_wrapper(*args, **kwargs):
|
|
154
|
+
name = span_name or f"{func.__module__}.{func.__name__}"
|
|
155
|
+
|
|
156
|
+
if not self.tracer:
|
|
157
|
+
return await func(*args, **kwargs)
|
|
158
|
+
|
|
159
|
+
with self.tracer.start_as_current_span(name) as span:
|
|
160
|
+
# Add attributes
|
|
161
|
+
if attributes:
|
|
162
|
+
for key, value in attributes.items():
|
|
163
|
+
span.set_attribute(key, value)
|
|
164
|
+
|
|
165
|
+
# Add function info
|
|
166
|
+
span.set_attribute("function.name", func.__name__)
|
|
167
|
+
span.set_attribute("function.module", func.__module__)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
result = await func(*args, **kwargs)
|
|
171
|
+
span.set_attribute("status", "success")
|
|
172
|
+
return result
|
|
173
|
+
except Exception as e:
|
|
174
|
+
span.set_attribute("status", "error")
|
|
175
|
+
span.set_attribute("error.type", type(e).__name__)
|
|
176
|
+
span.set_attribute("error.message", str(e))
|
|
177
|
+
span.record_exception(e)
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
@wraps(func)
|
|
181
|
+
def sync_wrapper(*args, **kwargs):
|
|
182
|
+
name = span_name or f"{func.__module__}.{func.__name__}"
|
|
183
|
+
|
|
184
|
+
if not self.tracer:
|
|
185
|
+
return func(*args, **kwargs)
|
|
186
|
+
|
|
187
|
+
with self.tracer.start_as_current_span(name) as span:
|
|
188
|
+
# Add attributes
|
|
189
|
+
if attributes:
|
|
190
|
+
for key, value in attributes.items():
|
|
191
|
+
span.set_attribute(key, value)
|
|
192
|
+
|
|
193
|
+
# Add function info
|
|
194
|
+
span.set_attribute("function.name", func.__name__)
|
|
195
|
+
span.set_attribute("function.module", func.__module__)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
result = func(*args, **kwargs)
|
|
199
|
+
span.set_attribute("status", "success")
|
|
200
|
+
return result
|
|
201
|
+
except Exception as e:
|
|
202
|
+
span.set_attribute("status", "error")
|
|
203
|
+
span.set_attribute("error.type", type(e).__name__)
|
|
204
|
+
span.set_attribute("error.message", str(e))
|
|
205
|
+
span.record_exception(e)
|
|
206
|
+
raise
|
|
207
|
+
|
|
208
|
+
# Return appropriate wrapper based on function type
|
|
209
|
+
import asyncio
|
|
210
|
+
if asyncio.iscoroutinefunction(func):
|
|
211
|
+
return async_wrapper
|
|
212
|
+
else:
|
|
213
|
+
return sync_wrapper
|
|
214
|
+
|
|
215
|
+
return decorator
|
|
216
|
+
|
|
217
|
+
def add_event(self, name: str, attributes: Optional[Dict[str, Any]] = None):
|
|
218
|
+
"""Add an event to the current span.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
name: Event name
|
|
222
|
+
attributes: Optional event attributes
|
|
223
|
+
"""
|
|
224
|
+
if not self.tracer:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
span = trace.get_current_span()
|
|
228
|
+
if span:
|
|
229
|
+
span.add_event(name, attributes=attributes or {})
|
|
230
|
+
|
|
231
|
+
def set_attribute(self, key: str, value: Any):
|
|
232
|
+
"""Set an attribute on the current span.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
key: Attribute key
|
|
236
|
+
value: Attribute value
|
|
237
|
+
"""
|
|
238
|
+
if not self.tracer:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
span = trace.get_current_span()
|
|
242
|
+
if span:
|
|
243
|
+
span.set_attribute(key, value)
|
|
244
|
+
|
|
245
|
+
def record_exception(self, exception: Exception):
|
|
246
|
+
"""Record an exception in the current span.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
exception: Exception to record
|
|
250
|
+
"""
|
|
251
|
+
if not self.tracer:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
span = trace.get_current_span()
|
|
255
|
+
if span:
|
|
256
|
+
span.record_exception(exception)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class _NoOpSpan:
|
|
260
|
+
"""No-op span when tracing is not available."""
|
|
261
|
+
|
|
262
|
+
def __enter__(self):
|
|
263
|
+
return self
|
|
264
|
+
|
|
265
|
+
def __exit__(self, *args):
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
def set_attribute(self, key: str, value: Any):
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
def add_event(self, name: str, attributes: Optional[Dict[str, Any]] = None):
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
def record_exception(self, exception: Exception):
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# Global tracing manager
|
|
279
|
+
_global_tracing = None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_tracing_manager() -> TracingManager:
|
|
283
|
+
"""Get global tracing manager.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Global tracing manager instance
|
|
287
|
+
"""
|
|
288
|
+
global _global_tracing
|
|
289
|
+
|
|
290
|
+
if _global_tracing is None:
|
|
291
|
+
# Initialize from environment variables
|
|
292
|
+
service_name = os.getenv("GENXAI_SERVICE_NAME", "genxai")
|
|
293
|
+
jaeger_host = os.getenv("JAEGER_AGENT_HOST", "localhost")
|
|
294
|
+
jaeger_port = int(os.getenv("JAEGER_AGENT_PORT", "6831"))
|
|
295
|
+
enable_console = os.getenv("GENXAI_TRACE_CONSOLE", "false").lower() == "true"
|
|
296
|
+
|
|
297
|
+
_global_tracing = TracingManager(
|
|
298
|
+
service_name=service_name,
|
|
299
|
+
jaeger_host=jaeger_host,
|
|
300
|
+
jaeger_port=jaeger_port,
|
|
301
|
+
enable_console=enable_console
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return _global_tracing
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def trace(span_name: Optional[str] = None, attributes: Optional[Dict[str, Any]] = None):
|
|
308
|
+
"""Decorator to trace a function.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
span_name: Optional span name (defaults to function name)
|
|
312
|
+
attributes: Optional span attributes
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Decorated function
|
|
316
|
+
"""
|
|
317
|
+
return get_tracing_manager().trace(span_name, attributes)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def start_span(name: str, attributes: Optional[Dict[str, Any]] = None):
|
|
321
|
+
"""Start a new span.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
name: Span name
|
|
325
|
+
attributes: Optional span attributes
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Span context manager
|
|
329
|
+
"""
|
|
330
|
+
return get_tracing_manager().start_span(name, attributes)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def span(name: str, attributes: Optional[Dict[str, Any]] = None):
|
|
334
|
+
"""Context manager for a tracing span.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
name: Span name
|
|
338
|
+
attributes: Optional span attributes
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Context manager yielding the span
|
|
342
|
+
"""
|
|
343
|
+
return get_tracing_manager().span(name, attributes)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def add_event(name: str, attributes: Optional[Dict[str, Any]] = None):
|
|
347
|
+
"""Add an event to the current span.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
name: Event name
|
|
351
|
+
attributes: Optional event attributes
|
|
352
|
+
"""
|
|
353
|
+
get_tracing_manager().add_event(name, attributes)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def set_attribute(key: str, value: Any):
|
|
357
|
+
"""Set an attribute on the current span.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
key: Attribute key
|
|
361
|
+
value: Attribute value
|
|
362
|
+
"""
|
|
363
|
+
get_tracing_manager().set_attribute(key, value)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def record_exception(exception: Exception):
|
|
367
|
+
"""Record an exception in the current span.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
exception: Exception to record
|
|
371
|
+
"""
|
|
372
|
+
get_tracing_manager().record_exception(exception)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Performance optimization utilities for GenXAI."""
|
|
2
|
+
|
|
3
|
+
from genxai.performance.cache import (
|
|
4
|
+
CacheManager,
|
|
5
|
+
MemoryCache,
|
|
6
|
+
RedisCache,
|
|
7
|
+
LRUCache,
|
|
8
|
+
cached,
|
|
9
|
+
get_cache_manager
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from genxai.performance.pooling import (
|
|
13
|
+
ConnectionPool,
|
|
14
|
+
DatabaseConnectionPool,
|
|
15
|
+
HTTPConnectionPool,
|
|
16
|
+
VectorStoreConnectionPool,
|
|
17
|
+
get_db_pool,
|
|
18
|
+
get_http_pool,
|
|
19
|
+
get_vector_pool
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# Caching
|
|
24
|
+
"CacheManager",
|
|
25
|
+
"MemoryCache",
|
|
26
|
+
"RedisCache",
|
|
27
|
+
"LRUCache",
|
|
28
|
+
"cached",
|
|
29
|
+
"get_cache_manager",
|
|
30
|
+
|
|
31
|
+
# Connection pooling
|
|
32
|
+
"ConnectionPool",
|
|
33
|
+
"DatabaseConnectionPool",
|
|
34
|
+
"HTTPConnectionPool",
|
|
35
|
+
"VectorStoreConnectionPool",
|
|
36
|
+
"get_db_pool",
|
|
37
|
+
"get_http_pool",
|
|
38
|
+
"get_vector_pool",
|
|
39
|
+
]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Caching system for GenXAI performance optimization."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import pickle
|
|
6
|
+
from typing import Any, Optional, Callable
|
|
7
|
+
from functools import wraps
|
|
8
|
+
import time
|
|
9
|
+
from datetime import timedelta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CacheBackend:
|
|
13
|
+
"""Base cache backend."""
|
|
14
|
+
|
|
15
|
+
def get(self, key: str) -> Optional[Any]:
|
|
16
|
+
"""Get value from cache."""
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None):
|
|
20
|
+
"""Set value in cache."""
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
def delete(self, key: str):
|
|
24
|
+
"""Delete value from cache."""
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
def clear(self):
|
|
28
|
+
"""Clear all cache."""
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MemoryCache(CacheBackend):
|
|
33
|
+
"""In-memory cache backend."""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self._cache = {}
|
|
37
|
+
self._expiry = {}
|
|
38
|
+
|
|
39
|
+
def get(self, key: str) -> Optional[Any]:
|
|
40
|
+
"""Get value from cache."""
|
|
41
|
+
# Check expiry
|
|
42
|
+
if key in self._expiry:
|
|
43
|
+
if time.time() > self._expiry[key]:
|
|
44
|
+
self.delete(key)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
return self._cache.get(key)
|
|
48
|
+
|
|
49
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None):
|
|
50
|
+
"""Set value in cache."""
|
|
51
|
+
self._cache[key] = value
|
|
52
|
+
|
|
53
|
+
if ttl:
|
|
54
|
+
self._expiry[key] = time.time() + ttl
|
|
55
|
+
|
|
56
|
+
def delete(self, key: str):
|
|
57
|
+
"""Delete value from cache."""
|
|
58
|
+
self._cache.pop(key, None)
|
|
59
|
+
self._expiry.pop(key, None)
|
|
60
|
+
|
|
61
|
+
def clear(self):
|
|
62
|
+
"""Clear all cache."""
|
|
63
|
+
self._cache.clear()
|
|
64
|
+
self._expiry.clear()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RedisCache(CacheBackend):
|
|
68
|
+
"""Redis cache backend."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, redis_url: str = "redis://localhost:6379"):
|
|
71
|
+
try:
|
|
72
|
+
import redis
|
|
73
|
+
self.client = redis.from_url(redis_url)
|
|
74
|
+
except ImportError:
|
|
75
|
+
raise ImportError("Redis not installed. Install with: pip install redis")
|
|
76
|
+
|
|
77
|
+
def get(self, key: str) -> Optional[Any]:
|
|
78
|
+
"""Get value from cache."""
|
|
79
|
+
value = self.client.get(key)
|
|
80
|
+
if value:
|
|
81
|
+
return pickle.loads(value)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None):
|
|
85
|
+
"""Set value in cache."""
|
|
86
|
+
serialized = pickle.dumps(value)
|
|
87
|
+
if ttl:
|
|
88
|
+
self.client.setex(key, ttl, serialized)
|
|
89
|
+
else:
|
|
90
|
+
self.client.set(key, serialized)
|
|
91
|
+
|
|
92
|
+
def delete(self, key: str):
|
|
93
|
+
"""Delete value from cache."""
|
|
94
|
+
self.client.delete(key)
|
|
95
|
+
|
|
96
|
+
def clear(self):
|
|
97
|
+
"""Clear all cache."""
|
|
98
|
+
self.client.flushdb()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class CacheManager:
|
|
102
|
+
"""Manage caching for different components."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, backend: CacheBackend):
|
|
105
|
+
self.backend = backend
|
|
106
|
+
|
|
107
|
+
def cache_key(self, prefix: str, *args, **kwargs) -> str:
|
|
108
|
+
"""Generate cache key."""
|
|
109
|
+
# Create deterministic key from arguments
|
|
110
|
+
key_data = {
|
|
111
|
+
"args": args,
|
|
112
|
+
"kwargs": sorted(kwargs.items())
|
|
113
|
+
}
|
|
114
|
+
key_str = json.dumps(key_data, sort_keys=True)
|
|
115
|
+
key_hash = hashlib.md5(key_str.encode()).hexdigest()
|
|
116
|
+
return f"{prefix}:{key_hash}"
|
|
117
|
+
|
|
118
|
+
def get(self, key: str) -> Optional[Any]:
|
|
119
|
+
"""Get from cache."""
|
|
120
|
+
return self.backend.get(key)
|
|
121
|
+
|
|
122
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None):
|
|
123
|
+
"""Set in cache."""
|
|
124
|
+
self.backend.set(key, value, ttl)
|
|
125
|
+
|
|
126
|
+
def delete(self, key: str):
|
|
127
|
+
"""Delete from cache."""
|
|
128
|
+
self.backend.delete(key)
|
|
129
|
+
|
|
130
|
+
def clear(self):
|
|
131
|
+
"""Clear cache."""
|
|
132
|
+
self.backend.clear()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Global cache manager
|
|
136
|
+
_cache_manager = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_cache_manager() -> CacheManager:
|
|
140
|
+
"""Get global cache manager."""
|
|
141
|
+
global _cache_manager
|
|
142
|
+
|
|
143
|
+
if _cache_manager is None:
|
|
144
|
+
import os
|
|
145
|
+
backend_type = os.getenv("CACHE_BACKEND", "memory")
|
|
146
|
+
|
|
147
|
+
if backend_type == "redis":
|
|
148
|
+
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379")
|
|
149
|
+
backend = RedisCache(redis_url)
|
|
150
|
+
else:
|
|
151
|
+
backend = MemoryCache()
|
|
152
|
+
|
|
153
|
+
_cache_manager = CacheManager(backend)
|
|
154
|
+
|
|
155
|
+
return _cache_manager
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cached(prefix: str, ttl: Optional[int] = 3600):
|
|
159
|
+
"""Decorator to cache function results.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
prefix: Cache key prefix
|
|
163
|
+
ttl: Time to live in seconds (default: 1 hour)
|
|
164
|
+
|
|
165
|
+
Usage:
|
|
166
|
+
@cached("llm_response", ttl=3600)
|
|
167
|
+
async def get_llm_response(prompt):
|
|
168
|
+
return await llm.generate(prompt)
|
|
169
|
+
"""
|
|
170
|
+
def decorator(func: Callable):
|
|
171
|
+
@wraps(func)
|
|
172
|
+
async def async_wrapper(*args, **kwargs):
|
|
173
|
+
cache = get_cache_manager()
|
|
174
|
+
|
|
175
|
+
# Generate cache key
|
|
176
|
+
cache_key = cache.cache_key(prefix, *args, **kwargs)
|
|
177
|
+
|
|
178
|
+
# Try to get from cache
|
|
179
|
+
cached_value = cache.get(cache_key)
|
|
180
|
+
if cached_value is not None:
|
|
181
|
+
return cached_value
|
|
182
|
+
|
|
183
|
+
# Call function
|
|
184
|
+
result = await func(*args, **kwargs)
|
|
185
|
+
|
|
186
|
+
# Store in cache
|
|
187
|
+
cache.set(cache_key, result, ttl)
|
|
188
|
+
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
@wraps(func)
|
|
192
|
+
def sync_wrapper(*args, **kwargs):
|
|
193
|
+
cache = get_cache_manager()
|
|
194
|
+
|
|
195
|
+
# Generate cache key
|
|
196
|
+
cache_key = cache.cache_key(prefix, *args, **kwargs)
|
|
197
|
+
|
|
198
|
+
# Try to get from cache
|
|
199
|
+
cached_value = cache.get(cache_key)
|
|
200
|
+
if cached_value is not None:
|
|
201
|
+
return cached_value
|
|
202
|
+
|
|
203
|
+
# Call function
|
|
204
|
+
result = func(*args, **kwargs)
|
|
205
|
+
|
|
206
|
+
# Store in cache
|
|
207
|
+
cache.set(cache_key, result, ttl)
|
|
208
|
+
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
# Return appropriate wrapper
|
|
212
|
+
import asyncio
|
|
213
|
+
if asyncio.iscoroutinefunction(func):
|
|
214
|
+
return async_wrapper
|
|
215
|
+
else:
|
|
216
|
+
return sync_wrapper
|
|
217
|
+
|
|
218
|
+
return decorator
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class LRUCache:
|
|
222
|
+
"""LRU (Least Recently Used) cache implementation."""
|
|
223
|
+
|
|
224
|
+
def __init__(self, capacity: int = 1000):
|
|
225
|
+
self.capacity = capacity
|
|
226
|
+
self.cache = {}
|
|
227
|
+
self.access_order = []
|
|
228
|
+
|
|
229
|
+
def get(self, key: str) -> Optional[Any]:
|
|
230
|
+
"""Get value and update access order."""
|
|
231
|
+
if key not in self.cache:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
# Update access order
|
|
235
|
+
self.access_order.remove(key)
|
|
236
|
+
self.access_order.append(key)
|
|
237
|
+
|
|
238
|
+
return self.cache[key]
|
|
239
|
+
|
|
240
|
+
def set(self, key: str, value: Any):
|
|
241
|
+
"""Set value and manage capacity."""
|
|
242
|
+
if key in self.cache:
|
|
243
|
+
# Update existing
|
|
244
|
+
self.access_order.remove(key)
|
|
245
|
+
elif len(self.cache) >= self.capacity:
|
|
246
|
+
# Evict least recently used
|
|
247
|
+
lru_key = self.access_order.pop(0)
|
|
248
|
+
del self.cache[lru_key]
|
|
249
|
+
|
|
250
|
+
self.cache[key] = value
|
|
251
|
+
self.access_order.append(key)
|
|
252
|
+
|
|
253
|
+
def clear(self):
|
|
254
|
+
"""Clear cache."""
|
|
255
|
+
self.cache.clear()
|
|
256
|
+
self.access_order.clear()
|