rakam-systems-core 0.1.1rc7__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 (34) hide show
  1. rakam_systems_core/__init__.py +41 -0
  2. rakam_systems_core/ai_core/__init__.py +68 -0
  3. rakam_systems_core/ai_core/base.py +142 -0
  4. rakam_systems_core/ai_core/config.py +12 -0
  5. rakam_systems_core/ai_core/config_loader.py +580 -0
  6. rakam_systems_core/ai_core/config_schema.py +395 -0
  7. rakam_systems_core/ai_core/interfaces/__init__.py +30 -0
  8. rakam_systems_core/ai_core/interfaces/agent.py +83 -0
  9. rakam_systems_core/ai_core/interfaces/chat_history.py +122 -0
  10. rakam_systems_core/ai_core/interfaces/chunker.py +11 -0
  11. rakam_systems_core/ai_core/interfaces/embedding_model.py +10 -0
  12. rakam_systems_core/ai_core/interfaces/indexer.py +10 -0
  13. rakam_systems_core/ai_core/interfaces/llm_gateway.py +139 -0
  14. rakam_systems_core/ai_core/interfaces/loader.py +86 -0
  15. rakam_systems_core/ai_core/interfaces/reranker.py +10 -0
  16. rakam_systems_core/ai_core/interfaces/retriever.py +11 -0
  17. rakam_systems_core/ai_core/interfaces/tool.py +162 -0
  18. rakam_systems_core/ai_core/interfaces/tool_invoker.py +260 -0
  19. rakam_systems_core/ai_core/interfaces/tool_loader.py +374 -0
  20. rakam_systems_core/ai_core/interfaces/tool_registry.py +287 -0
  21. rakam_systems_core/ai_core/interfaces/vectorstore.py +37 -0
  22. rakam_systems_core/ai_core/mcp/README.md +545 -0
  23. rakam_systems_core/ai_core/mcp/__init__.py +0 -0
  24. rakam_systems_core/ai_core/mcp/mcp_server.py +334 -0
  25. rakam_systems_core/ai_core/tracking.py +602 -0
  26. rakam_systems_core/ai_core/vs_core.py +55 -0
  27. rakam_systems_core/ai_utils/__init__.py +16 -0
  28. rakam_systems_core/ai_utils/logging.py +126 -0
  29. rakam_systems_core/ai_utils/metrics.py +10 -0
  30. rakam_systems_core/ai_utils/s3.py +480 -0
  31. rakam_systems_core/ai_utils/tracing.py +5 -0
  32. rakam_systems_core-0.1.1rc7.dist-info/METADATA +162 -0
  33. rakam_systems_core-0.1.1rc7.dist-info/RECORD +34 -0
  34. rakam_systems_core-0.1.1rc7.dist-info/WHEEL +4 -0
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Lightweight logging utilities for rakam_systems.
5
+
6
+ This module is a very thin wrapper around Python's standard :mod:`logging`
7
+ library that provides:
8
+
9
+ - A single place to configure default log format and level
10
+ - A stable import path for all internal modules:
11
+
12
+ >>> from rakam_systems_core.ai_utils import logging
13
+ >>> logger = logging.getLogger(__name__)
14
+ >>> logger.info("hello")
15
+
16
+ It intentionally mirrors the standard logging API so most existing code
17
+ can simply replace ``import logging`` with::
18
+
19
+ from rakam_systems_core.ai_utils import logging
20
+
21
+ and continue to work as before.
22
+ """
23
+
24
+ import logging as _logging
25
+ import os
26
+ from typing import Any, Optional
27
+
28
+ # Re-export common types and level constants so callers can use
29
+ # logging.INFO, logging.WARNING, etc.
30
+ Logger = _logging.Logger
31
+
32
+ DEBUG = _logging.DEBUG
33
+ INFO = _logging.INFO
34
+ WARNING = _logging.WARNING
35
+ ERROR = _logging.ERROR
36
+ CRITICAL = _logging.CRITICAL
37
+
38
+
39
+ _DEFAULT_LEVEL_NAME = os.getenv("RAKAM_LOG_LEVEL", "INFO").upper()
40
+ _DEFAULT_LEVEL = getattr(_logging, _DEFAULT_LEVEL_NAME, _logging.INFO)
41
+ _DEFAULT_FORMAT = os.getenv(
42
+ "RAKAM_LOG_FORMAT",
43
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
44
+ )
45
+ _DEFAULT_DATEFMT = os.getenv("RAKAM_LOG_DATEFMT", "%Y-%m-%d %H:%M:%S")
46
+
47
+
48
+ def basicConfig(
49
+ *,
50
+ level: int | str = _DEFAULT_LEVEL,
51
+ format: str = _DEFAULT_FORMAT,
52
+ datefmt: Optional[str] = _DEFAULT_DATEFMT,
53
+ **kwargs: Any,
54
+ ) -> None:
55
+ """
56
+ Configure the root logger.
57
+
58
+ This is a thin wrapper over :func:`logging.basicConfig` that also
59
+ accepts string log levels (\"INFO\", \"DEBUG\", ...).
60
+ """
61
+ if isinstance(level, str):
62
+ level = getattr(_logging, level.upper(), _DEFAULT_LEVEL)
63
+
64
+ _logging.basicConfig(level=level, format=format, datefmt=datefmt, **kwargs)
65
+
66
+
67
+ def _ensure_basic_config() -> None:
68
+ """
69
+ Ensure there is at least a minimal logging configuration.
70
+
71
+ If user code has already configured logging (root logger has handlers),
72
+ this function is a no-op.
73
+ """
74
+ root = _logging.getLogger()
75
+ if root.handlers:
76
+ return
77
+ basicConfig()
78
+
79
+
80
+ def get_logger(name: str = "ai") -> Logger:
81
+ """
82
+ Return a configured logger instance.
83
+
84
+ This is the preferred entry point for application code. It mirrors the
85
+ standard :func:`logging.getLogger` but ensures a basic configuration
86
+ exists first.
87
+ """
88
+ _ensure_basic_config()
89
+ return _logging.getLogger(name)
90
+
91
+
92
+ def getLogger(name: str = "ai") -> Logger:
93
+ """
94
+ Alias for :func:`get_logger` to match the stdlib API.
95
+
96
+ Allows existing code that uses ``logging.getLogger`` to keep working
97
+ when importing this module as ``logging``.
98
+ """
99
+ return get_logger(name)
100
+
101
+
102
+ def setLevel(level: int | str) -> None:
103
+ """
104
+ Convenience helper to set the global root log level.
105
+
106
+ Example:
107
+ >>> from rakam_systems_core.ai_utils import logging
108
+ >>> logging.setLevel("DEBUG")
109
+ """
110
+ if isinstance(level, str):
111
+ level = getattr(_logging, level.upper(), _DEFAULT_LEVEL)
112
+ _logging.getLogger().setLevel(level)
113
+
114
+
115
+ __all__ = [
116
+ "Logger",
117
+ "DEBUG",
118
+ "INFO",
119
+ "WARNING",
120
+ "ERROR",
121
+ "CRITICAL",
122
+ "basicConfig",
123
+ "get_logger",
124
+ "getLogger",
125
+ "setLevel",
126
+ ]
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+ from typing import Dict
3
+
4
+ _METRICS: Dict[str, float] = {}
5
+
6
+ def record_metric(name: str, value: float) -> None:
7
+ _METRICS[name] = value
8
+
9
+ def get_metric(name: str) -> float | None:
10
+ return _METRICS.get(name)
@@ -0,0 +1,480 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Lightweight S3 utilities for rakam_systems.
5
+
6
+ This module provides a thin wrapper around boto3's S3 client with:
7
+
8
+ - Environment-based configuration (credentials, endpoint, region, bucket)
9
+ - Simple CRUD operations (upload, download, delete, list)
10
+ - Support for S3-compatible services (OVH, Scaleway, MinIO, etc.)
11
+ - Consistent error handling and logging
12
+ - A stable import path for all internal modules:
13
+
14
+ >>> from rakam_systems_core.ai_utils import s3
15
+ >>> s3.upload_file("my-key.txt", "Hello World")
16
+ >>> content = s3.download_file("my-key.txt") # Returns bytes
17
+ >>> text = content.decode('utf-8') # Decode to string if needed
18
+
19
+ Configuration is read from environment variables:
20
+ - S3_ACCESS_KEY: AWS/S3 access key ID (required)
21
+ - S3_SECRET_KEY: AWS/S3 secret access key (required)
22
+ - S3_BUCKET_NAME: Default bucket name (required)
23
+ - S3_ENDPOINT_URL: Custom endpoint for S3-compatible services (optional)
24
+ - S3_REGION: AWS region or provider region code (default: "gra")
25
+ """
26
+
27
+ import os
28
+ from typing import Any, Dict, List, Optional
29
+ from datetime import datetime
30
+
31
+ try:
32
+ import boto3
33
+ from botocore.exceptions import ClientError
34
+ except ImportError:
35
+ raise ImportError(
36
+ "boto3 is required for S3 utilities. Install with: pip install boto3"
37
+ )
38
+
39
+
40
+ # Configuration from environment variables
41
+ _S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY")
42
+ _S3_SECRET_KEY = os.getenv("S3_SECRET_KEY")
43
+ _S3_ENDPOINT_URL = os.getenv(
44
+ "S3_ENDPOINT_URL", "https://s3.gra.io.cloud.ovh.net")
45
+ _S3_REGION = os.getenv("S3_REGION", "gra")
46
+ _S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME")
47
+
48
+ # Singleton client instance
49
+ _client: Optional[Any] = None
50
+
51
+
52
+ class S3Error(Exception):
53
+ """Base exception for S3 operations."""
54
+ pass
55
+
56
+
57
+ class S3ConfigError(S3Error):
58
+ """Raised when S3 configuration is invalid or missing."""
59
+ pass
60
+
61
+
62
+ class S3NotFoundError(S3Error):
63
+ """Raised when a requested S3 object is not found."""
64
+ pass
65
+
66
+
67
+ class S3PermissionError(S3Error):
68
+ """Raised when access to S3 resource is denied."""
69
+ pass
70
+
71
+
72
+ def _validate_config() -> None:
73
+ """
74
+ Validate that all required S3 configuration is present.
75
+
76
+ Raises:
77
+ S3ConfigError: If required configuration is missing.
78
+ """
79
+ missing = []
80
+ if not _S3_ACCESS_KEY:
81
+ missing.append("S3_ACCESS_KEY")
82
+ if not _S3_SECRET_KEY:
83
+ missing.append("S3_SECRET_KEY")
84
+ if not _S3_BUCKET_NAME:
85
+ missing.append("S3_BUCKET_NAME")
86
+
87
+ if missing:
88
+ raise S3ConfigError(
89
+ f"Missing required environment variables: {', '.join(missing)}. "
90
+ f"Please set these in your environment or .env file."
91
+ )
92
+
93
+
94
+ def get_client() -> Any:
95
+ """
96
+ Get or create the S3 client singleton.
97
+
98
+ Returns:
99
+ boto3.client: Configured S3 client instance.
100
+
101
+ Raises:
102
+ S3ConfigError: If required configuration is missing.
103
+ """
104
+ global _client
105
+
106
+ if _client is not None:
107
+ return _client
108
+
109
+ _validate_config()
110
+
111
+ client_config = {
112
+ 'aws_access_key_id': _S3_ACCESS_KEY,
113
+ 'aws_secret_access_key': _S3_SECRET_KEY,
114
+ 'region_name': _S3_REGION
115
+ }
116
+
117
+ # Add endpoint URL if specified (for S3-compatible services)
118
+ if _S3_ENDPOINT_URL:
119
+ client_config['endpoint_url'] = _S3_ENDPOINT_URL
120
+
121
+ _client = boto3.client('s3', **client_config)
122
+ return _client
123
+
124
+
125
+ def reset_client() -> None:
126
+ """
127
+ Reset the S3 client singleton.
128
+
129
+ Useful for testing or when credentials need to be refreshed.
130
+ """
131
+ global _client
132
+ _client = None
133
+
134
+
135
+ def upload_file(
136
+ key: str,
137
+ content: str | bytes,
138
+ bucket: Optional[str] = None,
139
+ content_type: str = "text/plain",
140
+ metadata: Optional[Dict[str, str]] = None
141
+ ) -> bool:
142
+ """
143
+ Upload a file to S3.
144
+
145
+ Args:
146
+ key: The S3 object key (path/filename).
147
+ content: File content as string or bytes.
148
+ bucket: Bucket name (defaults to S3_BUCKET_NAME).
149
+ content_type: MIME type of the content.
150
+ metadata: Optional metadata dictionary.
151
+
152
+ Returns:
153
+ bool: True if upload succeeded.
154
+
155
+ Raises:
156
+ S3Error: If upload fails.
157
+ """
158
+ client = get_client()
159
+ bucket = bucket or _S3_BUCKET_NAME
160
+
161
+ # Convert string to bytes if needed
162
+ if isinstance(content, str):
163
+ content = content.encode('utf-8')
164
+
165
+ try:
166
+ put_args = {
167
+ 'Bucket': bucket,
168
+ 'Key': key,
169
+ 'Body': content,
170
+ 'ContentType': content_type
171
+ }
172
+
173
+ if metadata:
174
+ put_args['Metadata'] = metadata
175
+
176
+ client.put_object(**put_args)
177
+ return True
178
+
179
+ except ClientError as e:
180
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
181
+ raise S3Error(f"Failed to upload file '{key}': {error_code} - {e}")
182
+
183
+
184
+ def download_file(
185
+ key: str,
186
+ bucket: Optional[str] = None
187
+ ) -> bytes:
188
+ """
189
+ Download a file from S3.
190
+
191
+ Args:
192
+ key: The S3 object key (path/filename).
193
+ bucket: Bucket name (defaults to S3_BUCKET_NAME).
194
+
195
+ Returns:
196
+ bytes: File content as bytes. The application should decode if needed
197
+ (e.g., content.decode('utf-8') for text files).
198
+
199
+ Raises:
200
+ S3NotFoundError: If file does not exist.
201
+ S3Error: If download fails.
202
+
203
+ Note:
204
+ This function always returns bytes to preserve data integrity and support
205
+ all file types (text, binary, images, PDFs, etc.). For text files, decode
206
+ the result: content.decode('utf-8')
207
+ """
208
+ client = get_client()
209
+ bucket = bucket or _S3_BUCKET_NAME
210
+
211
+ try:
212
+ response = client.get_object(Bucket=bucket, Key=key)
213
+ content = response['Body'].read()
214
+ return content
215
+
216
+ except ClientError as e:
217
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
218
+ if error_code == 'NoSuchKey' or error_code == '404':
219
+ raise S3NotFoundError(f"File not found: '{key}'")
220
+ raise S3Error(f"Failed to download file '{key}': {error_code} - {e}")
221
+
222
+
223
+ def delete_file(key: str, bucket: Optional[str] = None) -> bool:
224
+ """
225
+ Delete a file from S3.
226
+
227
+ Args:
228
+ key: The S3 object key (path/filename).
229
+ bucket: Bucket name (defaults to S3_BUCKET_NAME).
230
+
231
+ Returns:
232
+ bool: True if deletion succeeded.
233
+
234
+ Raises:
235
+ S3Error: If deletion fails.
236
+ """
237
+ client = get_client()
238
+ bucket = bucket or _S3_BUCKET_NAME
239
+
240
+ try:
241
+ client.delete_object(Bucket=bucket, Key=key)
242
+ return True
243
+
244
+ except ClientError as e:
245
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
246
+ raise S3Error(f"Failed to delete file '{key}': {error_code} - {e}")
247
+
248
+
249
+ def file_exists(key: str, bucket: Optional[str] = None) -> bool:
250
+ """
251
+ Check if a file exists in S3.
252
+
253
+ Args:
254
+ key: The S3 object key (path/filename).
255
+ bucket: Bucket name (defaults to S3_BUCKET_NAME).
256
+
257
+ Returns:
258
+ bool: True if file exists, False otherwise.
259
+ """
260
+ client = get_client()
261
+ bucket = bucket or _S3_BUCKET_NAME
262
+
263
+ try:
264
+ client.head_object(Bucket=bucket, Key=key)
265
+ return True
266
+ except ClientError as e:
267
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
268
+ if error_code == 'NoSuchKey' or error_code == '404':
269
+ return False
270
+ # Re-raise other errors
271
+ raise S3Error(
272
+ f"Failed to check file existence '{key}': {error_code} - {e}")
273
+
274
+
275
+ def list_files(
276
+ prefix: str = "",
277
+ bucket: Optional[str] = None,
278
+ max_keys: int = 1000
279
+ ) -> List[Dict[str, Any]]:
280
+ """
281
+ List files in S3 bucket with optional prefix filter.
282
+
283
+ Args:
284
+ prefix: Filter results to keys starting with this prefix.
285
+ bucket: Bucket name (defaults to S3_BUCKET_NAME).
286
+ max_keys: Maximum number of keys to return.
287
+
288
+ Returns:
289
+ List[Dict]: List of file metadata dictionaries with keys:
290
+ - Key: Object key (str)
291
+ - Size: File size in bytes (int)
292
+ - LastModified: Last modification datetime
293
+ - ETag: Entity tag (str)
294
+ """
295
+ client = get_client()
296
+ bucket = bucket or _S3_BUCKET_NAME
297
+
298
+ try:
299
+ response = client.list_objects_v2(
300
+ Bucket=bucket,
301
+ Prefix=prefix,
302
+ MaxKeys=max_keys
303
+ )
304
+
305
+ if 'Contents' not in response:
306
+ return []
307
+
308
+ return [
309
+ {
310
+ 'Key': obj['Key'],
311
+ 'Size': obj['Size'],
312
+ 'LastModified': obj['LastModified'],
313
+ 'ETag': obj.get('ETag', '')
314
+ }
315
+ for obj in response['Contents']
316
+ ]
317
+
318
+ except ClientError as e:
319
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
320
+ raise S3Error(
321
+ f"Failed to list files with prefix '{prefix}': {error_code} - {e}")
322
+
323
+
324
+ def get_file_metadata(key: str, bucket: Optional[str] = None) -> Dict[str, Any]:
325
+ """
326
+ Get metadata for a file without downloading it.
327
+
328
+ Args:
329
+ key: The S3 object key (path/filename).
330
+ bucket: Bucket name (defaults to S3_BUCKET_NAME).
331
+
332
+ Returns:
333
+ Dict: Metadata dictionary with keys like ContentType, ContentLength,
334
+ LastModified, ETag, Metadata (custom metadata).
335
+
336
+ Raises:
337
+ S3NotFoundError: If file does not exist.
338
+ S3Error: If operation fails.
339
+ """
340
+ client = get_client()
341
+ bucket = bucket or _S3_BUCKET_NAME
342
+
343
+ try:
344
+ response = client.head_object(Bucket=bucket, Key=key)
345
+ return {
346
+ 'ContentType': response.get('ContentType', ''),
347
+ 'ContentLength': response.get('ContentLength', 0),
348
+ 'LastModified': response.get('LastModified'),
349
+ 'ETag': response.get('ETag', ''),
350
+ 'Metadata': response.get('Metadata', {})
351
+ }
352
+
353
+ except ClientError as e:
354
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
355
+ if error_code == 'NoSuchKey' or error_code == '404':
356
+ raise S3NotFoundError(f"File not found: '{key}'")
357
+ raise S3Error(
358
+ f"Failed to get metadata for '{key}': {error_code} - {e}")
359
+
360
+
361
+ def create_bucket(bucket: Optional[str] = None) -> bool:
362
+ """
363
+ Create an S3 bucket.
364
+
365
+ Args:
366
+ bucket: Bucket name (defaults to S3_BUCKET_NAME).
367
+
368
+ Returns:
369
+ bool: True if bucket was created or already exists.
370
+
371
+ Raises:
372
+ S3Error: If bucket creation fails.
373
+ """
374
+ client = get_client()
375
+ bucket = bucket or _S3_BUCKET_NAME
376
+
377
+ try:
378
+ client.create_bucket(Bucket=bucket)
379
+ return True
380
+
381
+ except ClientError as e:
382
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
383
+
384
+ # Bucket already exists cases
385
+ if error_code in ('BucketAlreadyOwnedByYou', 'BucketAlreadyExists'):
386
+ return True
387
+
388
+ raise S3Error(
389
+ f"Failed to create bucket '{bucket}': {error_code} - {e}")
390
+
391
+
392
+ def bucket_exists(bucket: Optional[str] = None) -> bool:
393
+ """
394
+ Check if a bucket exists and is accessible.
395
+
396
+ Args:
397
+ bucket: Bucket name (defaults to S3_BUCKET_NAME).
398
+
399
+ Returns:
400
+ bool: True if bucket exists and is accessible.
401
+ """
402
+ client = get_client()
403
+ bucket = bucket or _S3_BUCKET_NAME
404
+
405
+ try:
406
+ client.head_bucket(Bucket=bucket)
407
+ return True
408
+ except ClientError as e:
409
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
410
+ if error_code in ('404', 'NoSuchBucket'):
411
+ return False
412
+ if error_code == '403':
413
+ raise S3PermissionError(f"Access denied to bucket '{bucket}'")
414
+ raise S3Error(f"Failed to check bucket '{bucket}': {error_code} - {e}")
415
+
416
+
417
+ def list_buckets() -> List[Dict[str, Any]]:
418
+ """
419
+ List all buckets in the account.
420
+
421
+ Returns:
422
+ List[Dict]: List of bucket metadata dictionaries with keys:
423
+ - Name: Bucket name (str)
424
+ - CreationDate: Creation datetime
425
+ """
426
+ client = get_client()
427
+
428
+ try:
429
+ response = client.list_buckets()
430
+ return [
431
+ {
432
+ 'Name': bucket['Name'],
433
+ 'CreationDate': bucket['CreationDate']
434
+ }
435
+ for bucket in response.get('Buckets', [])
436
+ ]
437
+
438
+ except ClientError as e:
439
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
440
+ raise S3Error(f"Failed to list buckets: {error_code} - {e}")
441
+
442
+
443
+ def get_config() -> Dict[str, Any]:
444
+ """
445
+ Get current S3 configuration.
446
+
447
+ Returns:
448
+ Dict: Configuration dictionary (secrets are masked).
449
+ """
450
+ return {
451
+ 'bucket': _S3_BUCKET_NAME,
452
+ 'region': _S3_REGION,
453
+ 'endpoint_url': _S3_ENDPOINT_URL,
454
+ 'access_key': _S3_ACCESS_KEY[:4] + '***' if _S3_ACCESS_KEY else None,
455
+ 'has_secret_key': bool(_S3_SECRET_KEY)
456
+ }
457
+
458
+
459
+ __all__ = [
460
+ # Exceptions
461
+ "S3Error",
462
+ "S3ConfigError",
463
+ "S3NotFoundError",
464
+ "S3PermissionError",
465
+ # Client management
466
+ "get_client",
467
+ "reset_client",
468
+ "get_config",
469
+ # File operations
470
+ "upload_file",
471
+ "download_file",
472
+ "delete_file",
473
+ "file_exists",
474
+ "list_files",
475
+ "get_file_metadata",
476
+ # Bucket operations
477
+ "create_bucket",
478
+ "bucket_exists",
479
+ "list_buckets",
480
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+ # Intentionally minimal; extend with real tracing later.
3
+
4
+ def init_tracing(service_name: str = "ai_system") -> None:
5
+ print(f"Tracing initialized for {service_name} (noop)")