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,289 @@
|
|
|
1
|
+
"""Connection pooling for GenXAI performance optimization."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Optional, Any, Dict
|
|
5
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
6
|
+
import queue
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConnectionPool:
|
|
11
|
+
"""Generic connection pool."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
create_connection,
|
|
16
|
+
min_size: int = 5,
|
|
17
|
+
max_size: int = 20,
|
|
18
|
+
timeout: float = 30.0
|
|
19
|
+
):
|
|
20
|
+
"""Initialize connection pool.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
create_connection: Function to create new connection
|
|
24
|
+
min_size: Minimum pool size
|
|
25
|
+
max_size: Maximum pool size
|
|
26
|
+
timeout: Connection timeout
|
|
27
|
+
"""
|
|
28
|
+
self.create_connection = create_connection
|
|
29
|
+
self.min_size = min_size
|
|
30
|
+
self.max_size = max_size
|
|
31
|
+
self.timeout = timeout
|
|
32
|
+
|
|
33
|
+
self._pool = asyncio.Queue(maxsize=max_size)
|
|
34
|
+
self._size = 0
|
|
35
|
+
self._lock = asyncio.Lock()
|
|
36
|
+
|
|
37
|
+
async def initialize(self):
|
|
38
|
+
"""Initialize pool with minimum connections."""
|
|
39
|
+
for _ in range(self.min_size):
|
|
40
|
+
conn = await self.create_connection()
|
|
41
|
+
await self._pool.put(conn)
|
|
42
|
+
self._size += 1
|
|
43
|
+
|
|
44
|
+
async def acquire(self):
|
|
45
|
+
"""Acquire connection from pool."""
|
|
46
|
+
try:
|
|
47
|
+
# Try to get existing connection
|
|
48
|
+
conn = await asyncio.wait_for(
|
|
49
|
+
self._pool.get(),
|
|
50
|
+
timeout=self.timeout
|
|
51
|
+
)
|
|
52
|
+
return conn
|
|
53
|
+
except asyncio.TimeoutError:
|
|
54
|
+
# Create new connection if under max size
|
|
55
|
+
async with self._lock:
|
|
56
|
+
if self._size < self.max_size:
|
|
57
|
+
conn = await self.create_connection()
|
|
58
|
+
self._size += 1
|
|
59
|
+
return conn
|
|
60
|
+
|
|
61
|
+
# Wait for available connection
|
|
62
|
+
conn = await self._pool.get()
|
|
63
|
+
return conn
|
|
64
|
+
|
|
65
|
+
async def release(self, conn):
|
|
66
|
+
"""Release connection back to pool."""
|
|
67
|
+
await self._pool.put(conn)
|
|
68
|
+
|
|
69
|
+
async def close(self):
|
|
70
|
+
"""Close all connections."""
|
|
71
|
+
while not self._pool.empty():
|
|
72
|
+
conn = await self._pool.get()
|
|
73
|
+
if hasattr(conn, 'close'):
|
|
74
|
+
await conn.close()
|
|
75
|
+
self._size = 0
|
|
76
|
+
|
|
77
|
+
@asynccontextmanager
|
|
78
|
+
async def connection(self):
|
|
79
|
+
"""Context manager for connection."""
|
|
80
|
+
conn = await self.acquire()
|
|
81
|
+
try:
|
|
82
|
+
yield conn
|
|
83
|
+
finally:
|
|
84
|
+
await self.release(conn)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class DatabaseConnectionPool:
|
|
88
|
+
"""Database connection pool."""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
db_url: str,
|
|
93
|
+
min_size: int = 5,
|
|
94
|
+
max_size: int = 20
|
|
95
|
+
):
|
|
96
|
+
"""Initialize database connection pool.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
db_url: Database URL
|
|
100
|
+
min_size: Minimum pool size
|
|
101
|
+
max_size: Maximum pool size
|
|
102
|
+
"""
|
|
103
|
+
self.db_url = db_url
|
|
104
|
+
self.min_size = min_size
|
|
105
|
+
self.max_size = max_size
|
|
106
|
+
self._pool = None
|
|
107
|
+
|
|
108
|
+
async def initialize(self):
|
|
109
|
+
"""Initialize pool."""
|
|
110
|
+
try:
|
|
111
|
+
import asyncpg
|
|
112
|
+
self._pool = await asyncpg.create_pool(
|
|
113
|
+
self.db_url,
|
|
114
|
+
min_size=self.min_size,
|
|
115
|
+
max_size=self.max_size
|
|
116
|
+
)
|
|
117
|
+
except ImportError:
|
|
118
|
+
raise ImportError("asyncpg not installed. Install with: pip install asyncpg")
|
|
119
|
+
|
|
120
|
+
@asynccontextmanager
|
|
121
|
+
async def connection(self):
|
|
122
|
+
"""Get connection from pool."""
|
|
123
|
+
async with self._pool.acquire() as conn:
|
|
124
|
+
yield conn
|
|
125
|
+
|
|
126
|
+
async def close(self):
|
|
127
|
+
"""Close pool."""
|
|
128
|
+
if self._pool:
|
|
129
|
+
await self._pool.close()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class HTTPConnectionPool:
|
|
133
|
+
"""HTTP connection pool using aiohttp."""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
connector_limit: int = 100,
|
|
138
|
+
connector_limit_per_host: int = 30
|
|
139
|
+
):
|
|
140
|
+
"""Initialize HTTP connection pool.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
connector_limit: Total connection limit
|
|
144
|
+
connector_limit_per_host: Per-host connection limit
|
|
145
|
+
"""
|
|
146
|
+
self.connector_limit = connector_limit
|
|
147
|
+
self.connector_limit_per_host = connector_limit_per_host
|
|
148
|
+
self._session = None
|
|
149
|
+
|
|
150
|
+
async def initialize(self):
|
|
151
|
+
"""Initialize session."""
|
|
152
|
+
try:
|
|
153
|
+
import aiohttp
|
|
154
|
+
connector = aiohttp.TCPConnector(
|
|
155
|
+
limit=self.connector_limit,
|
|
156
|
+
limit_per_host=self.connector_limit_per_host
|
|
157
|
+
)
|
|
158
|
+
self._session = aiohttp.ClientSession(connector=connector)
|
|
159
|
+
except ImportError:
|
|
160
|
+
raise ImportError("aiohttp not installed. Install with: pip install aiohttp")
|
|
161
|
+
|
|
162
|
+
async def get(self, url: str, **kwargs):
|
|
163
|
+
"""GET request."""
|
|
164
|
+
async with self._session.get(url, **kwargs) as response:
|
|
165
|
+
return await response.json()
|
|
166
|
+
|
|
167
|
+
async def post(self, url: str, **kwargs):
|
|
168
|
+
"""POST request."""
|
|
169
|
+
async with self._session.post(url, **kwargs) as response:
|
|
170
|
+
return await response.json()
|
|
171
|
+
|
|
172
|
+
async def close(self):
|
|
173
|
+
"""Close session."""
|
|
174
|
+
if self._session:
|
|
175
|
+
await self._session.close()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class VectorStoreConnectionPool:
|
|
179
|
+
"""Vector store connection pool."""
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
store_type: str,
|
|
184
|
+
config: Dict[str, Any],
|
|
185
|
+
pool_size: int = 10
|
|
186
|
+
):
|
|
187
|
+
"""Initialize vector store connection pool.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
store_type: Vector store type (chromadb, pinecone, weaviate)
|
|
191
|
+
config: Store configuration
|
|
192
|
+
pool_size: Pool size
|
|
193
|
+
"""
|
|
194
|
+
self.store_type = store_type
|
|
195
|
+
self.config = config
|
|
196
|
+
self.pool_size = pool_size
|
|
197
|
+
self._connections = queue.Queue(maxsize=pool_size)
|
|
198
|
+
self._lock = threading.Lock()
|
|
199
|
+
|
|
200
|
+
def initialize(self):
|
|
201
|
+
"""Initialize pool."""
|
|
202
|
+
for _ in range(self.pool_size):
|
|
203
|
+
conn = self._create_connection()
|
|
204
|
+
self._connections.put(conn)
|
|
205
|
+
|
|
206
|
+
def _create_connection(self):
|
|
207
|
+
"""Create vector store connection."""
|
|
208
|
+
if self.store_type == "chromadb":
|
|
209
|
+
import chromadb
|
|
210
|
+
return chromadb.Client(self.config)
|
|
211
|
+
elif self.store_type == "pinecone":
|
|
212
|
+
import pinecone
|
|
213
|
+
pinecone.init(**self.config)
|
|
214
|
+
return pinecone
|
|
215
|
+
elif self.store_type == "weaviate":
|
|
216
|
+
import weaviate
|
|
217
|
+
return weaviate.Client(**self.config)
|
|
218
|
+
else:
|
|
219
|
+
raise ValueError(f"Unknown store type: {self.store_type}")
|
|
220
|
+
|
|
221
|
+
@contextmanager
|
|
222
|
+
def connection(self):
|
|
223
|
+
"""Get connection from pool."""
|
|
224
|
+
conn = self._connections.get(timeout=30)
|
|
225
|
+
try:
|
|
226
|
+
yield conn
|
|
227
|
+
finally:
|
|
228
|
+
self._connections.put(conn)
|
|
229
|
+
|
|
230
|
+
def close(self):
|
|
231
|
+
"""Close all connections."""
|
|
232
|
+
while not self._connections.empty():
|
|
233
|
+
conn = self._connections.get()
|
|
234
|
+
if hasattr(conn, 'close'):
|
|
235
|
+
conn.close()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# Global pools
|
|
239
|
+
_db_pool = None
|
|
240
|
+
_http_pool = None
|
|
241
|
+
_vector_pool = None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def get_db_pool() -> DatabaseConnectionPool:
|
|
245
|
+
"""Get global database pool."""
|
|
246
|
+
global _db_pool
|
|
247
|
+
|
|
248
|
+
if _db_pool is None:
|
|
249
|
+
import os
|
|
250
|
+
db_url = os.getenv("POSTGRES_URL", "postgresql://localhost/genxai")
|
|
251
|
+
_db_pool = DatabaseConnectionPool(db_url)
|
|
252
|
+
await _db_pool.initialize()
|
|
253
|
+
|
|
254
|
+
return _db_pool
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def get_http_pool() -> HTTPConnectionPool:
|
|
258
|
+
"""Get global HTTP pool."""
|
|
259
|
+
global _http_pool
|
|
260
|
+
|
|
261
|
+
if _http_pool is None:
|
|
262
|
+
_http_pool = HTTPConnectionPool()
|
|
263
|
+
await _http_pool.initialize()
|
|
264
|
+
|
|
265
|
+
return _http_pool
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_vector_pool(store_type: str = "chromadb") -> VectorStoreConnectionPool:
|
|
269
|
+
"""Get global vector store pool."""
|
|
270
|
+
global _vector_pool
|
|
271
|
+
|
|
272
|
+
if _vector_pool is None:
|
|
273
|
+
import os
|
|
274
|
+
config = {}
|
|
275
|
+
|
|
276
|
+
if store_type == "pinecone":
|
|
277
|
+
config = {
|
|
278
|
+
"api_key": os.getenv("PINECONE_API_KEY"),
|
|
279
|
+
"environment": os.getenv("PINECONE_ENV", "us-west1-gcp")
|
|
280
|
+
}
|
|
281
|
+
elif store_type == "weaviate":
|
|
282
|
+
config = {
|
|
283
|
+
"url": os.getenv("WEAVIATE_URL", "http://localhost:8080")
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_vector_pool = VectorStoreConnectionPool(store_type, config)
|
|
287
|
+
_vector_pool.initialize()
|
|
288
|
+
|
|
289
|
+
return _vector_pool
|
genxai/security/audit.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Audit logging and approvals for governance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sqlite3
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AuditEvent:
|
|
16
|
+
action: str
|
|
17
|
+
actor_id: str
|
|
18
|
+
resource_id: str
|
|
19
|
+
status: str
|
|
20
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ApprovalRequest:
|
|
26
|
+
request_id: str
|
|
27
|
+
action: str
|
|
28
|
+
resource_id: str
|
|
29
|
+
actor_id: str
|
|
30
|
+
status: str = "pending"
|
|
31
|
+
created_at: datetime = field(default_factory=datetime.utcnow)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuditStore:
|
|
35
|
+
"""SQLite-backed store for audit logs and approvals."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, db_path: Path) -> None:
|
|
38
|
+
self.db_path = db_path
|
|
39
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
self._ensure_db()
|
|
41
|
+
|
|
42
|
+
def _connect(self) -> sqlite3.Connection:
|
|
43
|
+
return sqlite3.connect(self.db_path)
|
|
44
|
+
|
|
45
|
+
def _with_connection(self, handler):
|
|
46
|
+
conn = self._connect()
|
|
47
|
+
try:
|
|
48
|
+
return handler(conn)
|
|
49
|
+
finally:
|
|
50
|
+
conn.close()
|
|
51
|
+
|
|
52
|
+
def _ensure_db(self) -> None:
|
|
53
|
+
def _init(conn: sqlite3.Connection) -> None:
|
|
54
|
+
cursor = conn.cursor()
|
|
55
|
+
cursor.execute(
|
|
56
|
+
"""
|
|
57
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
action TEXT NOT NULL,
|
|
60
|
+
actor_id TEXT NOT NULL,
|
|
61
|
+
resource_id TEXT NOT NULL,
|
|
62
|
+
status TEXT NOT NULL,
|
|
63
|
+
metadata TEXT NOT NULL,
|
|
64
|
+
timestamp TEXT NOT NULL
|
|
65
|
+
)
|
|
66
|
+
"""
|
|
67
|
+
)
|
|
68
|
+
cursor.execute(
|
|
69
|
+
"""
|
|
70
|
+
CREATE TABLE IF NOT EXISTS approval_requests (
|
|
71
|
+
request_id TEXT PRIMARY KEY,
|
|
72
|
+
action TEXT NOT NULL,
|
|
73
|
+
resource_id TEXT NOT NULL,
|
|
74
|
+
actor_id TEXT NOT NULL,
|
|
75
|
+
status TEXT NOT NULL,
|
|
76
|
+
created_at TEXT NOT NULL
|
|
77
|
+
)
|
|
78
|
+
"""
|
|
79
|
+
)
|
|
80
|
+
conn.commit()
|
|
81
|
+
|
|
82
|
+
self._with_connection(_init)
|
|
83
|
+
|
|
84
|
+
def load_events(self) -> List[AuditEvent]:
|
|
85
|
+
def _load(conn: sqlite3.Connection):
|
|
86
|
+
cursor = conn.cursor()
|
|
87
|
+
cursor.execute(
|
|
88
|
+
"SELECT action, actor_id, resource_id, status, metadata, timestamp FROM audit_events"
|
|
89
|
+
)
|
|
90
|
+
return cursor.fetchall()
|
|
91
|
+
|
|
92
|
+
rows = self._with_connection(_load)
|
|
93
|
+
events: List[AuditEvent] = []
|
|
94
|
+
for action, actor_id, resource_id, status, metadata, timestamp in rows:
|
|
95
|
+
events.append(
|
|
96
|
+
AuditEvent(
|
|
97
|
+
action=action,
|
|
98
|
+
actor_id=actor_id,
|
|
99
|
+
resource_id=resource_id,
|
|
100
|
+
status=status,
|
|
101
|
+
metadata=json.loads(metadata),
|
|
102
|
+
timestamp=datetime.fromisoformat(timestamp),
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
return events
|
|
106
|
+
|
|
107
|
+
def save_event(self, event: AuditEvent) -> None:
|
|
108
|
+
def _save(conn: sqlite3.Connection) -> None:
|
|
109
|
+
cursor = conn.cursor()
|
|
110
|
+
cursor.execute(
|
|
111
|
+
"""
|
|
112
|
+
INSERT INTO audit_events (action, actor_id, resource_id, status, metadata, timestamp)
|
|
113
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
114
|
+
""",
|
|
115
|
+
(
|
|
116
|
+
event.action,
|
|
117
|
+
event.actor_id,
|
|
118
|
+
event.resource_id,
|
|
119
|
+
event.status,
|
|
120
|
+
json.dumps(event.metadata, default=str),
|
|
121
|
+
event.timestamp.isoformat(),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
conn.commit()
|
|
125
|
+
|
|
126
|
+
self._with_connection(_save)
|
|
127
|
+
|
|
128
|
+
def clear_events(self) -> None:
|
|
129
|
+
def _clear(conn: sqlite3.Connection) -> None:
|
|
130
|
+
cursor = conn.cursor()
|
|
131
|
+
cursor.execute("DELETE FROM audit_events")
|
|
132
|
+
conn.commit()
|
|
133
|
+
|
|
134
|
+
self._with_connection(_clear)
|
|
135
|
+
|
|
136
|
+
def load_requests(self) -> Dict[str, ApprovalRequest]:
|
|
137
|
+
def _load(conn: sqlite3.Connection):
|
|
138
|
+
cursor = conn.cursor()
|
|
139
|
+
cursor.execute(
|
|
140
|
+
"SELECT request_id, action, resource_id, actor_id, status, created_at FROM approval_requests"
|
|
141
|
+
)
|
|
142
|
+
return cursor.fetchall()
|
|
143
|
+
|
|
144
|
+
rows = self._with_connection(_load)
|
|
145
|
+
requests: Dict[str, ApprovalRequest] = {}
|
|
146
|
+
for request_id, action, resource_id, actor_id, status, created_at in rows:
|
|
147
|
+
requests[request_id] = ApprovalRequest(
|
|
148
|
+
request_id=request_id,
|
|
149
|
+
action=action,
|
|
150
|
+
resource_id=resource_id,
|
|
151
|
+
actor_id=actor_id,
|
|
152
|
+
status=status,
|
|
153
|
+
created_at=datetime.fromisoformat(created_at),
|
|
154
|
+
)
|
|
155
|
+
return requests
|
|
156
|
+
|
|
157
|
+
def save_request(self, request: ApprovalRequest) -> None:
|
|
158
|
+
def _save(conn: sqlite3.Connection) -> None:
|
|
159
|
+
cursor = conn.cursor()
|
|
160
|
+
cursor.execute(
|
|
161
|
+
"""
|
|
162
|
+
INSERT OR REPLACE INTO approval_requests
|
|
163
|
+
(request_id, action, resource_id, actor_id, status, created_at)
|
|
164
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
165
|
+
""",
|
|
166
|
+
(
|
|
167
|
+
request.request_id,
|
|
168
|
+
request.action,
|
|
169
|
+
request.resource_id,
|
|
170
|
+
request.actor_id,
|
|
171
|
+
request.status,
|
|
172
|
+
request.created_at.isoformat(),
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
conn.commit()
|
|
176
|
+
|
|
177
|
+
self._with_connection(_save)
|
|
178
|
+
|
|
179
|
+
def clear_requests(self) -> None:
|
|
180
|
+
def _clear(conn: sqlite3.Connection) -> None:
|
|
181
|
+
cursor = conn.cursor()
|
|
182
|
+
cursor.execute("DELETE FROM approval_requests")
|
|
183
|
+
conn.commit()
|
|
184
|
+
|
|
185
|
+
self._with_connection(_clear)
|
|
186
|
+
|
|
187
|
+
def vacuum(self) -> None:
|
|
188
|
+
def _vacuum(conn: sqlite3.Connection) -> None:
|
|
189
|
+
conn.execute("VACUUM")
|
|
190
|
+
|
|
191
|
+
self._with_connection(_vacuum)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class AuditLog:
|
|
195
|
+
"""In-memory audit log."""
|
|
196
|
+
|
|
197
|
+
def __init__(self) -> None:
|
|
198
|
+
self._store = _get_audit_store()
|
|
199
|
+
self._events: List[AuditEvent] = self._store.load_events()
|
|
200
|
+
|
|
201
|
+
def record(self, event: AuditEvent) -> None:
|
|
202
|
+
self._events.append(event)
|
|
203
|
+
self._store.save_event(event)
|
|
204
|
+
|
|
205
|
+
def list_events(self) -> List[AuditEvent]:
|
|
206
|
+
self._events = self._store.load_events()
|
|
207
|
+
return list(self._events)
|
|
208
|
+
|
|
209
|
+
def clear(self) -> None:
|
|
210
|
+
self._events.clear()
|
|
211
|
+
self._store.clear_events()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ApprovalService:
|
|
215
|
+
"""Simple in-memory approval workflow."""
|
|
216
|
+
|
|
217
|
+
def __init__(self) -> None:
|
|
218
|
+
self._store = _get_audit_store()
|
|
219
|
+
self._requests: Dict[str, ApprovalRequest] = self._store.load_requests()
|
|
220
|
+
self._counter = self._infer_counter()
|
|
221
|
+
|
|
222
|
+
def _infer_counter(self) -> int:
|
|
223
|
+
max_counter = 0
|
|
224
|
+
for request_id in self._requests:
|
|
225
|
+
if request_id.startswith("approval_"):
|
|
226
|
+
try:
|
|
227
|
+
max_counter = max(max_counter, int(request_id.split("_", 1)[1]))
|
|
228
|
+
except ValueError:
|
|
229
|
+
continue
|
|
230
|
+
return max_counter
|
|
231
|
+
|
|
232
|
+
def submit(self, action: str, resource_id: str, actor_id: str) -> ApprovalRequest:
|
|
233
|
+
self._counter += 1
|
|
234
|
+
request_id = f"approval_{self._counter}"
|
|
235
|
+
request = ApprovalRequest(
|
|
236
|
+
request_id=request_id,
|
|
237
|
+
action=action,
|
|
238
|
+
resource_id=resource_id,
|
|
239
|
+
actor_id=actor_id,
|
|
240
|
+
)
|
|
241
|
+
self._requests[request_id] = request
|
|
242
|
+
self._store.save_request(request)
|
|
243
|
+
return request
|
|
244
|
+
|
|
245
|
+
def approve(self, request_id: str) -> Optional[ApprovalRequest]:
|
|
246
|
+
request = self._requests.get(request_id)
|
|
247
|
+
if request:
|
|
248
|
+
request.status = "approved"
|
|
249
|
+
self._store.save_request(request)
|
|
250
|
+
return request
|
|
251
|
+
|
|
252
|
+
def reject(self, request_id: str) -> Optional[ApprovalRequest]:
|
|
253
|
+
request = self._requests.get(request_id)
|
|
254
|
+
if request:
|
|
255
|
+
request.status = "rejected"
|
|
256
|
+
self._store.save_request(request)
|
|
257
|
+
return request
|
|
258
|
+
|
|
259
|
+
def get(self, request_id: str) -> Optional[ApprovalRequest]:
|
|
260
|
+
return self._requests.get(request_id)
|
|
261
|
+
|
|
262
|
+
def clear(self) -> None:
|
|
263
|
+
self._requests.clear()
|
|
264
|
+
self._counter = 0
|
|
265
|
+
self._store.clear_requests()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
_audit_log: Optional[AuditLog] = None
|
|
269
|
+
_approval_service: Optional[ApprovalService] = None
|
|
270
|
+
_audit_store: Optional[AuditStore] = None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _get_default_db_path() -> Path:
|
|
274
|
+
return Path(__file__).resolve().parents[1] / "data" / "audit.db"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _get_audit_store() -> AuditStore:
|
|
278
|
+
global _audit_store
|
|
279
|
+
if _audit_store is None:
|
|
280
|
+
db_path = Path(os.getenv("GENXAI_AUDIT_DB", str(_get_default_db_path())))
|
|
281
|
+
_audit_store = AuditStore(db_path)
|
|
282
|
+
return _audit_store
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def get_audit_log() -> AuditLog:
|
|
286
|
+
global _audit_log
|
|
287
|
+
if _audit_log is None:
|
|
288
|
+
_audit_log = AuditLog()
|
|
289
|
+
return _audit_log
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_approval_service() -> ApprovalService:
|
|
293
|
+
global _approval_service
|
|
294
|
+
if _approval_service is None:
|
|
295
|
+
_approval_service = ApprovalService()
|
|
296
|
+
return _approval_service
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def reset_audit_services() -> None:
|
|
300
|
+
"""Reset audit services (useful for tests)."""
|
|
301
|
+
global _audit_log, _approval_service, _audit_store
|
|
302
|
+
_audit_log = None
|
|
303
|
+
_approval_service = None
|
|
304
|
+
_audit_store = None
|