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.
Files changed (156) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +6 -0
  3. cli/commands/approval.py +85 -0
  4. cli/commands/audit.py +127 -0
  5. cli/commands/metrics.py +25 -0
  6. cli/commands/tool.py +389 -0
  7. cli/main.py +32 -0
  8. genxai/__init__.py +81 -0
  9. genxai/api/__init__.py +5 -0
  10. genxai/api/app.py +21 -0
  11. genxai/config/__init__.py +5 -0
  12. genxai/config/settings.py +37 -0
  13. genxai/connectors/__init__.py +19 -0
  14. genxai/connectors/base.py +122 -0
  15. genxai/connectors/kafka.py +92 -0
  16. genxai/connectors/postgres_cdc.py +95 -0
  17. genxai/connectors/registry.py +44 -0
  18. genxai/connectors/sqs.py +94 -0
  19. genxai/connectors/webhook.py +73 -0
  20. genxai/core/__init__.py +37 -0
  21. genxai/core/agent/__init__.py +32 -0
  22. genxai/core/agent/base.py +206 -0
  23. genxai/core/agent/config_io.py +59 -0
  24. genxai/core/agent/registry.py +98 -0
  25. genxai/core/agent/runtime.py +970 -0
  26. genxai/core/communication/__init__.py +6 -0
  27. genxai/core/communication/collaboration.py +44 -0
  28. genxai/core/communication/message_bus.py +192 -0
  29. genxai/core/communication/protocols.py +35 -0
  30. genxai/core/execution/__init__.py +22 -0
  31. genxai/core/execution/metadata.py +181 -0
  32. genxai/core/execution/queue.py +201 -0
  33. genxai/core/graph/__init__.py +30 -0
  34. genxai/core/graph/checkpoints.py +77 -0
  35. genxai/core/graph/edges.py +131 -0
  36. genxai/core/graph/engine.py +813 -0
  37. genxai/core/graph/executor.py +516 -0
  38. genxai/core/graph/nodes.py +161 -0
  39. genxai/core/graph/trigger_runner.py +40 -0
  40. genxai/core/memory/__init__.py +19 -0
  41. genxai/core/memory/base.py +72 -0
  42. genxai/core/memory/embedding.py +327 -0
  43. genxai/core/memory/episodic.py +448 -0
  44. genxai/core/memory/long_term.py +467 -0
  45. genxai/core/memory/manager.py +543 -0
  46. genxai/core/memory/persistence.py +297 -0
  47. genxai/core/memory/procedural.py +461 -0
  48. genxai/core/memory/semantic.py +526 -0
  49. genxai/core/memory/shared.py +62 -0
  50. genxai/core/memory/short_term.py +303 -0
  51. genxai/core/memory/vector_store.py +508 -0
  52. genxai/core/memory/working.py +211 -0
  53. genxai/core/state/__init__.py +6 -0
  54. genxai/core/state/manager.py +293 -0
  55. genxai/core/state/schema.py +115 -0
  56. genxai/llm/__init__.py +14 -0
  57. genxai/llm/base.py +150 -0
  58. genxai/llm/factory.py +329 -0
  59. genxai/llm/providers/__init__.py +1 -0
  60. genxai/llm/providers/anthropic.py +249 -0
  61. genxai/llm/providers/cohere.py +274 -0
  62. genxai/llm/providers/google.py +334 -0
  63. genxai/llm/providers/ollama.py +147 -0
  64. genxai/llm/providers/openai.py +257 -0
  65. genxai/llm/routing.py +83 -0
  66. genxai/observability/__init__.py +6 -0
  67. genxai/observability/logging.py +327 -0
  68. genxai/observability/metrics.py +494 -0
  69. genxai/observability/tracing.py +372 -0
  70. genxai/performance/__init__.py +39 -0
  71. genxai/performance/cache.py +256 -0
  72. genxai/performance/pooling.py +289 -0
  73. genxai/security/audit.py +304 -0
  74. genxai/security/auth.py +315 -0
  75. genxai/security/cost_control.py +528 -0
  76. genxai/security/default_policies.py +44 -0
  77. genxai/security/jwt.py +142 -0
  78. genxai/security/oauth.py +226 -0
  79. genxai/security/pii.py +366 -0
  80. genxai/security/policy_engine.py +82 -0
  81. genxai/security/rate_limit.py +341 -0
  82. genxai/security/rbac.py +247 -0
  83. genxai/security/validation.py +218 -0
  84. genxai/tools/__init__.py +21 -0
  85. genxai/tools/base.py +383 -0
  86. genxai/tools/builtin/__init__.py +131 -0
  87. genxai/tools/builtin/communication/__init__.py +15 -0
  88. genxai/tools/builtin/communication/email_sender.py +159 -0
  89. genxai/tools/builtin/communication/notification_manager.py +167 -0
  90. genxai/tools/builtin/communication/slack_notifier.py +118 -0
  91. genxai/tools/builtin/communication/sms_sender.py +118 -0
  92. genxai/tools/builtin/communication/webhook_caller.py +136 -0
  93. genxai/tools/builtin/computation/__init__.py +15 -0
  94. genxai/tools/builtin/computation/calculator.py +101 -0
  95. genxai/tools/builtin/computation/code_executor.py +183 -0
  96. genxai/tools/builtin/computation/data_validator.py +259 -0
  97. genxai/tools/builtin/computation/hash_generator.py +129 -0
  98. genxai/tools/builtin/computation/regex_matcher.py +201 -0
  99. genxai/tools/builtin/data/__init__.py +15 -0
  100. genxai/tools/builtin/data/csv_processor.py +213 -0
  101. genxai/tools/builtin/data/data_transformer.py +299 -0
  102. genxai/tools/builtin/data/json_processor.py +233 -0
  103. genxai/tools/builtin/data/text_analyzer.py +288 -0
  104. genxai/tools/builtin/data/xml_processor.py +175 -0
  105. genxai/tools/builtin/database/__init__.py +15 -0
  106. genxai/tools/builtin/database/database_inspector.py +157 -0
  107. genxai/tools/builtin/database/mongodb_query.py +196 -0
  108. genxai/tools/builtin/database/redis_cache.py +167 -0
  109. genxai/tools/builtin/database/sql_query.py +145 -0
  110. genxai/tools/builtin/database/vector_search.py +163 -0
  111. genxai/tools/builtin/file/__init__.py +17 -0
  112. genxai/tools/builtin/file/directory_scanner.py +214 -0
  113. genxai/tools/builtin/file/file_compressor.py +237 -0
  114. genxai/tools/builtin/file/file_reader.py +102 -0
  115. genxai/tools/builtin/file/file_writer.py +122 -0
  116. genxai/tools/builtin/file/image_processor.py +186 -0
  117. genxai/tools/builtin/file/pdf_parser.py +144 -0
  118. genxai/tools/builtin/test/__init__.py +15 -0
  119. genxai/tools/builtin/test/async_simulator.py +62 -0
  120. genxai/tools/builtin/test/data_transformer.py +99 -0
  121. genxai/tools/builtin/test/error_generator.py +82 -0
  122. genxai/tools/builtin/test/simple_math.py +94 -0
  123. genxai/tools/builtin/test/string_processor.py +72 -0
  124. genxai/tools/builtin/web/__init__.py +15 -0
  125. genxai/tools/builtin/web/api_caller.py +161 -0
  126. genxai/tools/builtin/web/html_parser.py +330 -0
  127. genxai/tools/builtin/web/http_client.py +187 -0
  128. genxai/tools/builtin/web/url_validator.py +162 -0
  129. genxai/tools/builtin/web/web_scraper.py +170 -0
  130. genxai/tools/custom/my_test_tool_2.py +9 -0
  131. genxai/tools/dynamic.py +105 -0
  132. genxai/tools/mcp_server.py +167 -0
  133. genxai/tools/persistence/__init__.py +6 -0
  134. genxai/tools/persistence/models.py +55 -0
  135. genxai/tools/persistence/service.py +322 -0
  136. genxai/tools/registry.py +227 -0
  137. genxai/tools/security/__init__.py +11 -0
  138. genxai/tools/security/limits.py +214 -0
  139. genxai/tools/security/policy.py +20 -0
  140. genxai/tools/security/sandbox.py +248 -0
  141. genxai/tools/templates.py +435 -0
  142. genxai/triggers/__init__.py +19 -0
  143. genxai/triggers/base.py +104 -0
  144. genxai/triggers/file_watcher.py +75 -0
  145. genxai/triggers/queue.py +68 -0
  146. genxai/triggers/registry.py +82 -0
  147. genxai/triggers/schedule.py +66 -0
  148. genxai/triggers/webhook.py +68 -0
  149. genxai/utils/__init__.py +1 -0
  150. genxai/utils/tokens.py +295 -0
  151. genxai_framework-0.1.0.dist-info/METADATA +495 -0
  152. genxai_framework-0.1.0.dist-info/RECORD +156 -0
  153. genxai_framework-0.1.0.dist-info/WHEEL +5 -0
  154. genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
  155. genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
  156. 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
@@ -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