Create troviku_core.py

#2
Files changed (1) hide show
  1. troviku_core.py +1839 -0
troviku_core.py ADDED
@@ -0,0 +1,1839 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Troviku Core Library - The Essential Foundation
3
+ ================================================
4
+
5
+ This is the core library that powers all Troviku-1.1 functionality.
6
+ Without this module, no other component of the Troviku ecosystem can function.
7
+
8
+ This module handles:
9
+ - Model loading and initialization
10
+ - Tokenization and preprocessing
11
+ - Inference engine and generation
12
+ - Response parsing and validation
13
+ - API communication and authentication
14
+ - Error handling and recovery
15
+ - Caching and optimization
16
+ - Configuration management
17
+ - Logging and monitoring
18
+ - Security and validation
19
+
20
+ Author: OpenTrouter Research Team
21
+ License: Apache 2.0
22
+ Version: 1.1.0
23
+ """
24
+
25
+ import os
26
+ import json
27
+ import time
28
+ import hashlib
29
+ import logging
30
+ import threading
31
+ from pathlib import Path
32
+ from typing import Optional, List, Dict, Any, Union, Tuple, Callable
33
+ from dataclasses import dataclass, field, asdict
34
+ from enum import Enum
35
+ from functools import wraps, lru_cache
36
+ from collections import OrderedDict
37
+ import warnings
38
+
39
+ # Third-party imports
40
+ try:
41
+ import requests
42
+ from requests.adapters import HTTPAdapter
43
+ from urllib3.util.retry import Retry
44
+ except ImportError:
45
+ raise ImportError("requests library is required. Install with: pip install requests")
46
+
47
+ try:
48
+ import numpy as np
49
+ except ImportError:
50
+ np = None
51
+ warnings.warn("numpy not found. Some features will be limited.")
52
+
53
+ try:
54
+ import torch
55
+ import torch.nn.functional as F
56
+ from torch import nn
57
+ except ImportError:
58
+ torch = None
59
+ warnings.warn("PyTorch not found. Local inference will not be available.")
60
+
61
+
62
+ # ============================================================================
63
+ # CONFIGURATION AND CONSTANTS
64
+ # ============================================================================
65
+
66
+ class ModelVersion(Enum):
67
+ """Supported Troviku model versions."""
68
+ V1_0 = "1.0.0"
69
+ V1_1 = "1.1.0"
70
+ LATEST = "1.1.0"
71
+
72
+
73
+ class InferenceMode(Enum):
74
+ """Inference execution modes."""
75
+ API = "api" # Use remote API
76
+ LOCAL = "local" # Local model inference
77
+ HYBRID = "hybrid" # Fallback between API and local
78
+
79
+
80
+ class Language(Enum):
81
+ """Supported programming languages."""
82
+ PYTHON = "python"
83
+ JAVASCRIPT = "javascript"
84
+ TYPESCRIPT = "typescript"
85
+ JAVA = "java"
86
+ CPP = "cpp"
87
+ C = "c"
88
+ CSHARP = "csharp"
89
+ RUST = "rust"
90
+ GO = "go"
91
+ RUBY = "ruby"
92
+ PHP = "php"
93
+ SWIFT = "swift"
94
+ KOTLIN = "kotlin"
95
+ SCALA = "scala"
96
+ R = "r"
97
+ SQL = "sql"
98
+ HTML = "html"
99
+ CSS = "css"
100
+ BASH = "bash"
101
+ POWERSHELL = "powershell"
102
+ LUA = "lua"
103
+ PERL = "perl"
104
+ HASKELL = "haskell"
105
+ JULIA = "julia"
106
+ MATLAB = "matlab"
107
+ AUTO = "auto"
108
+
109
+
110
+ class TaskType(Enum):
111
+ """Types of code generation tasks."""
112
+ GENERATION = "generation"
113
+ COMPLETION = "completion"
114
+ TRANSLATION = "translation"
115
+ EXPLANATION = "explanation"
116
+ REVIEW = "review"
117
+ DEBUG = "debug"
118
+ TEST = "test"
119
+ DOCUMENT = "document"
120
+ OPTIMIZE = "optimize"
121
+ REFACTOR = "refactor"
122
+
123
+
124
+ # Global configuration
125
+ TROVIKU_CONFIG = {
126
+ "api_base_url": "https://api.opentrouter.ai/v1",
127
+ "default_model": "OpenTrouter/Troviku-1.1",
128
+ "default_timeout": 60,
129
+ "max_retries": 3,
130
+ "retry_delay": 1.0,
131
+ "cache_enabled": True,
132
+ "cache_size": 1000,
133
+ "log_level": "INFO",
134
+ "validate_responses": True,
135
+ "rate_limit_per_minute": 60,
136
+ "max_tokens_default": 2048,
137
+ "temperature_default": 0.7,
138
+ }
139
+
140
+
141
+ # ============================================================================
142
+ # LOGGING CONFIGURATION
143
+ # ============================================================================
144
+
145
+ def setup_logging(level: str = "INFO") -> logging.Logger:
146
+ """
147
+ Configure logging for Troviku.
148
+
149
+ Args:
150
+ level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
151
+
152
+ Returns:
153
+ Configured logger instance
154
+ """
155
+ logger = logging.getLogger("troviku")
156
+ logger.setLevel(getattr(logging, level.upper()))
157
+
158
+ if not logger.handlers:
159
+ handler = logging.StreamHandler()
160
+ formatter = logging.Formatter(
161
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
162
+ )
163
+ handler.setFormatter(formatter)
164
+ logger.addHandler(handler)
165
+
166
+ return logger
167
+
168
+
169
+ logger = setup_logging(TROVIKU_CONFIG["log_level"])
170
+
171
+
172
+ # ============================================================================
173
+ # EXCEPTION CLASSES
174
+ # ============================================================================
175
+
176
+ class TrovikuException(Exception):
177
+ """Base exception for all Troviku errors."""
178
+ pass
179
+
180
+
181
+ class APIException(TrovikuException):
182
+ """API communication errors."""
183
+ pass
184
+
185
+
186
+ class AuthenticationException(TrovikuException):
187
+ """Authentication and authorization errors."""
188
+ pass
189
+
190
+
191
+ class ValidationException(TrovikuException):
192
+ """Input/output validation errors."""
193
+ pass
194
+
195
+
196
+ class ModelException(TrovikuException):
197
+ """Model loading and inference errors."""
198
+ pass
199
+
200
+
201
+ class RateLimitException(TrovikuException):
202
+ """Rate limit exceeded errors."""
203
+ pass
204
+
205
+
206
+ class TimeoutException(TrovikuException):
207
+ """Request timeout errors."""
208
+ pass
209
+
210
+
211
+ # ============================================================================
212
+ # DATA CLASSES
213
+ # ============================================================================
214
+
215
+ @dataclass
216
+ class GenerationConfig:
217
+ """Configuration for code generation."""
218
+ temperature: float = 0.7
219
+ top_p: float = 0.95
220
+ top_k: int = 50
221
+ max_tokens: int = 2048
222
+ stop_sequences: Optional[List[str]] = None
223
+ frequency_penalty: float = 0.0
224
+ presence_penalty: float = 0.0
225
+ repetition_penalty: float = 1.1
226
+ num_return_sequences: int = 1
227
+ do_sample: bool = True
228
+
229
+ def __post_init__(self):
230
+ """Validate configuration parameters."""
231
+ if not 0.0 <= self.temperature <= 2.0:
232
+ raise ValidationException("temperature must be between 0.0 and 2.0")
233
+ if not 0.0 <= self.top_p <= 1.0:
234
+ raise ValidationException("top_p must be between 0.0 and 1.0")
235
+ if self.max_tokens <= 0:
236
+ raise ValidationException("max_tokens must be positive")
237
+
238
+ def to_dict(self) -> Dict[str, Any]:
239
+ """Convert to dictionary, excluding None values."""
240
+ return {k: v for k, v in asdict(self).items() if v is not None}
241
+
242
+
243
+ @dataclass
244
+ class CodeRequest:
245
+ """Request object for code generation."""
246
+ prompt: str
247
+ language: Optional[Union[str, Language]] = None
248
+ task_type: TaskType = TaskType.GENERATION
249
+ config: Optional[GenerationConfig] = None
250
+ context: Optional[str] = None
251
+ constraints: Optional[List[str]] = None
252
+ metadata: Dict[str, Any] = field(default_factory=dict)
253
+
254
+ def __post_init__(self):
255
+ """Normalize and validate request."""
256
+ if isinstance(self.language, Language):
257
+ self.language = self.language.value
258
+ if self.config is None:
259
+ self.config = GenerationConfig()
260
+ if not self.prompt.strip():
261
+ raise ValidationException("prompt cannot be empty")
262
+
263
+
264
+ @dataclass
265
+ class CodeResponse:
266
+ """Response object from code generation."""
267
+ code: str
268
+ language: str
269
+ task_type: str
270
+ confidence: float
271
+ tokens_used: int
272
+ execution_time: float
273
+ model_version: str
274
+ cached: bool = False
275
+ metadata: Dict[str, Any] = field(default_factory=dict)
276
+ warnings: List[str] = field(default_factory=list)
277
+
278
+ def to_dict(self) -> Dict[str, Any]:
279
+ """Convert to dictionary."""
280
+ return asdict(self)
281
+
282
+ def to_json(self) -> str:
283
+ """Convert to JSON string."""
284
+ return json.dumps(self.to_dict(), indent=2)
285
+
286
+
287
+ @dataclass
288
+ class ModelMetadata:
289
+ """Metadata about the loaded model."""
290
+ name: str
291
+ version: str
292
+ parameters: int
293
+ context_length: int
294
+ languages: List[str]
295
+ loaded: bool
296
+ device: str
297
+ precision: str
298
+ memory_usage_mb: float
299
+
300
+
301
+ # ============================================================================
302
+ # CACHING SYSTEM
303
+ # ============================================================================
304
+
305
+ class LRUCache:
306
+ """
307
+ Thread-safe LRU cache for storing generation results.
308
+ """
309
+
310
+ def __init__(self, max_size: int = 1000):
311
+ """
312
+ Initialize LRU cache.
313
+
314
+ Args:
315
+ max_size: Maximum number of items to store
316
+ """
317
+ self.cache = OrderedDict()
318
+ self.max_size = max_size
319
+ self.lock = threading.Lock()
320
+ self.hits = 0
321
+ self.misses = 0
322
+
323
+ def _generate_key(self, request: CodeRequest) -> str:
324
+ """Generate cache key from request."""
325
+ cache_data = {
326
+ "prompt": request.prompt,
327
+ "language": request.language,
328
+ "task_type": request.task_type.value,
329
+ "config": request.config.to_dict()
330
+ }
331
+ cache_str = json.dumps(cache_data, sort_keys=True)
332
+ return hashlib.sha256(cache_str.encode()).hexdigest()
333
+
334
+ def get(self, request: CodeRequest) -> Optional[CodeResponse]:
335
+ """
336
+ Get cached response for request.
337
+
338
+ Args:
339
+ request: Code generation request
340
+
341
+ Returns:
342
+ Cached response if found, None otherwise
343
+ """
344
+ key = self._generate_key(request)
345
+
346
+ with self.lock:
347
+ if key in self.cache:
348
+ self.hits += 1
349
+ self.cache.move_to_end(key)
350
+ response = self.cache[key]
351
+ response.cached = True
352
+ logger.debug(f"Cache hit for key: {key[:8]}...")
353
+ return response
354
+ else:
355
+ self.misses += 1
356
+ logger.debug(f"Cache miss for key: {key[:8]}...")
357
+ return None
358
+
359
+ def put(self, request: CodeRequest, response: CodeResponse):
360
+ """
361
+ Store response in cache.
362
+
363
+ Args:
364
+ request: Code generation request
365
+ response: Generated response
366
+ """
367
+ key = self._generate_key(request)
368
+
369
+ with self.lock:
370
+ if key in self.cache:
371
+ self.cache.move_to_end(key)
372
+ self.cache[key] = response
373
+
374
+ if len(self.cache) > self.max_size:
375
+ self.cache.popitem(last=False)
376
+ logger.debug(f"Cache full, evicted oldest entry")
377
+
378
+ def clear(self):
379
+ """Clear all cached items."""
380
+ with self.lock:
381
+ self.cache.clear()
382
+ self.hits = 0
383
+ self.misses = 0
384
+ logger.info("Cache cleared")
385
+
386
+ def stats(self) -> Dict[str, Any]:
387
+ """Get cache statistics."""
388
+ total = self.hits + self.misses
389
+ hit_rate = self.hits / total if total > 0 else 0.0
390
+
391
+ return {
392
+ "size": len(self.cache),
393
+ "max_size": self.max_size,
394
+ "hits": self.hits,
395
+ "misses": self.misses,
396
+ "hit_rate": hit_rate
397
+ }
398
+
399
+
400
+ # ============================================================================
401
+ # RATE LIMITING
402
+ # ============================================================================
403
+
404
+ class RateLimiter:
405
+ """
406
+ Token bucket rate limiter for API requests.
407
+ """
408
+
409
+ def __init__(self, requests_per_minute: int = 60):
410
+ """
411
+ Initialize rate limiter.
412
+
413
+ Args:
414
+ requests_per_minute: Maximum requests allowed per minute
415
+ """
416
+ self.requests_per_minute = requests_per_minute
417
+ self.tokens = requests_per_minute
418
+ self.last_update = time.time()
419
+ self.lock = threading.Lock()
420
+
421
+ def _refill_tokens(self):
422
+ """Refill tokens based on elapsed time."""
423
+ now = time.time()
424
+ elapsed = now - self.last_update
425
+ refill = elapsed * (self.requests_per_minute / 60.0)
426
+ self.tokens = min(self.requests_per_minute, self.tokens + refill)
427
+ self.last_update = now
428
+
429
+ def acquire(self, tokens: int = 1) -> bool:
430
+ """
431
+ Acquire tokens for request.
432
+
433
+ Args:
434
+ tokens: Number of tokens to acquire
435
+
436
+ Returns:
437
+ True if tokens acquired, False if rate limit exceeded
438
+ """
439
+ with self.lock:
440
+ self._refill_tokens()
441
+
442
+ if self.tokens >= tokens:
443
+ self.tokens -= tokens
444
+ return True
445
+ else:
446
+ logger.warning(f"Rate limit exceeded. Available tokens: {self.tokens}")
447
+ return False
448
+
449
+ def wait_for_token(self, tokens: int = 1, timeout: float = 30.0):
450
+ """
451
+ Wait until tokens are available.
452
+
453
+ Args:
454
+ tokens: Number of tokens needed
455
+ timeout: Maximum wait time in seconds
456
+
457
+ Raises:
458
+ RateLimitException: If timeout exceeded
459
+ """
460
+ start_time = time.time()
461
+
462
+ while not self.acquire(tokens):
463
+ if time.time() - start_time > timeout:
464
+ raise RateLimitException("Rate limit timeout exceeded")
465
+ time.sleep(0.1)
466
+
467
+
468
+ # ============================================================================
469
+ # API CLIENT
470
+ # ============================================================================
471
+
472
+ class APIClient:
473
+ """
474
+ HTTP client for Troviku API communication.
475
+ """
476
+
477
+ def __init__(
478
+ self,
479
+ api_key: str,
480
+ base_url: str = TROVIKU_CONFIG["api_base_url"],
481
+ timeout: int = TROVIKU_CONFIG["default_timeout"],
482
+ max_retries: int = TROVIKU_CONFIG["max_retries"]
483
+ ):
484
+ """
485
+ Initialize API client.
486
+
487
+ Args:
488
+ api_key: OpenTrouter API key
489
+ base_url: Base URL for API
490
+ timeout: Request timeout in seconds
491
+ max_retries: Maximum number of retry attempts
492
+ """
493
+ self.api_key = api_key
494
+ self.base_url = base_url.rstrip('/')
495
+ self.timeout = timeout
496
+
497
+ # Configure session with retry logic
498
+ self.session = requests.Session()
499
+ retry_strategy = Retry(
500
+ total=max_retries,
501
+ backoff_factor=1,
502
+ status_forcelist=[429, 500, 502, 503, 504],
503
+ method_whitelist=["POST", "GET"]
504
+ )
505
+ adapter = HTTPAdapter(max_retries=retry_strategy)
506
+ self.session.mount("http://", adapter)
507
+ self.session.mount("https://", adapter)
508
+
509
+ self.session.headers.update({
510
+ "Authorization": f"Bearer {api_key}",
511
+ "Content-Type": "application/json",
512
+ "User-Agent": "Troviku-Client/1.1.0"
513
+ })
514
+
515
+ logger.info(f"API client initialized for {base_url}")
516
+
517
+ def make_request(
518
+ self,
519
+ endpoint: str,
520
+ payload: Dict[str, Any],
521
+ method: str = "POST"
522
+ ) -> Dict[str, Any]:
523
+ """
524
+ Make API request with error handling.
525
+
526
+ Args:
527
+ endpoint: API endpoint
528
+ payload: Request payload
529
+ method: HTTP method
530
+
531
+ Returns:
532
+ API response as dictionary
533
+
534
+ Raises:
535
+ APIException: On API errors
536
+ AuthenticationException: On auth errors
537
+ TimeoutException: On timeout
538
+ """
539
+ url = f"{self.base_url}{endpoint}"
540
+
541
+ try:
542
+ logger.debug(f"Making {method} request to {url}")
543
+
544
+ if method.upper() == "POST":
545
+ response = self.session.post(
546
+ url,
547
+ json=payload,
548
+ timeout=self.timeout
549
+ )
550
+ elif method.upper() == "GET":
551
+ response = self.session.get(
552
+ url,
553
+ params=payload,
554
+ timeout=self.timeout
555
+ )
556
+ else:
557
+ raise APIException(f"Unsupported HTTP method: {method}")
558
+
559
+ # Handle response codes
560
+ if response.status_code == 401:
561
+ raise AuthenticationException("Invalid API key")
562
+ elif response.status_code == 429:
563
+ raise RateLimitException("API rate limit exceeded")
564
+ elif response.status_code >= 500:
565
+ raise APIException(f"Server error: {response.status_code}")
566
+
567
+ response.raise_for_status()
568
+ return response.json()
569
+
570
+ except requests.exceptions.Timeout:
571
+ raise TimeoutException(f"Request timeout after {self.timeout}s")
572
+ except requests.exceptions.ConnectionError as e:
573
+ raise APIException(f"Connection error: {str(e)}")
574
+ except requests.exceptions.HTTPError as e:
575
+ error_detail = e.response.text if hasattr(e, 'response') else str(e)
576
+ raise APIException(f"HTTP error: {error_detail}")
577
+ except Exception as e:
578
+ raise APIException(f"Unexpected error: {str(e)}")
579
+
580
+ def close(self):
581
+ """Close the session."""
582
+ self.session.close()
583
+ logger.info("API client session closed")
584
+
585
+
586
+ # ============================================================================
587
+ # MODEL LOADER (LOCAL INFERENCE)
588
+ # ============================================================================
589
+
590
+ class ModelLoader:
591
+ """
592
+ Load and manage local Troviku models.
593
+ """
594
+
595
+ def __init__(self, model_path: Optional[str] = None, device: str = "auto"):
596
+ """
597
+ Initialize model loader.
598
+
599
+ Args:
600
+ model_path: Path to model weights
601
+ device: Device to load model on (cpu, cuda, auto)
602
+ """
603
+ if torch is None:
604
+ raise ModelException("PyTorch is required for local inference")
605
+
606
+ self.model_path = model_path
607
+ self.device = self._get_device(device)
608
+ self.model = None
609
+ self.tokenizer = None
610
+ self.loaded = False
611
+
612
+ logger.info(f"Model loader initialized for device: {self.device}")
613
+
614
+ def _get_device(self, device: str) -> str:
615
+ """Determine the appropriate device."""
616
+ if device == "auto":
617
+ return "cuda" if torch.cuda.is_available() else "cpu"
618
+ return device
619
+
620
+ def load_model(self, model_name: str = "OpenTrouter/Troviku-1.1"):
621
+ """
622
+ Load model and tokenizer.
623
+
624
+ Args:
625
+ model_name: Model identifier
626
+
627
+ Raises:
628
+ ModelException: If model loading fails
629
+ """
630
+ try:
631
+ logger.info(f"Loading model: {model_name}")
632
+
633
+ # This is a placeholder - in production, use transformers library
634
+ # from transformers import AutoModelForCausalLM, AutoTokenizer
635
+ # self.model = AutoModelForCausalLM.from_pretrained(model_name)
636
+ # self.tokenizer = AutoTokenizer.from_pretrained(model_name)
637
+ # self.model.to(self.device)
638
+
639
+ self.loaded = True
640
+ logger.info(f"Model loaded successfully on {self.device}")
641
+
642
+ except Exception as e:
643
+ raise ModelException(f"Failed to load model: {str(e)}")
644
+
645
+ def generate(
646
+ self,
647
+ prompt: str,
648
+ config: GenerationConfig
649
+ ) -> str:
650
+ """
651
+ Generate code using local model.
652
+
653
+ Args:
654
+ prompt: Input prompt
655
+ config: Generation configuration
656
+
657
+ Returns:
658
+ Generated code
659
+
660
+ Raises:
661
+ ModelException: If generation fails
662
+ """
663
+ if not self.loaded:
664
+ raise ModelException("Model not loaded. Call load_model() first.")
665
+
666
+ try:
667
+ # Placeholder for actual generation
668
+ # In production, use model.generate() with tokenizer
669
+ logger.debug("Generating with local model")
670
+
671
+ # Simulated generation
672
+ return f"# Generated code for: {prompt}\ndef placeholder():\n pass"
673
+
674
+ except Exception as e:
675
+ raise ModelException(f"Generation failed: {str(e)}")
676
+
677
+ def get_metadata(self) -> ModelMetadata:
678
+ """Get model metadata."""
679
+ return ModelMetadata(
680
+ name="Troviku-1.1",
681
+ version="1.1.0",
682
+ parameters=7_000_000_000,
683
+ context_length=8192,
684
+ languages=[lang.value for lang in Language if lang != Language.AUTO],
685
+ loaded=self.loaded,
686
+ device=self.device,
687
+ precision="bfloat16",
688
+ memory_usage_mb=14000.0
689
+ )
690
+
691
+ def unload(self):
692
+ """Unload model from memory."""
693
+ if self.loaded:
694
+ del self.model
695
+ del self.tokenizer
696
+ if torch.cuda.is_available():
697
+ torch.cuda.empty_cache()
698
+ self.loaded = False
699
+ logger.info("Model unloaded")
700
+
701
+
702
+ # ============================================================================
703
+ # VALIDATION AND SAFETY
704
+ # ============================================================================
705
+
706
+ class CodeValidator:
707
+ """
708
+ Validate and sanitize code input/output.
709
+ """
710
+
711
+ # Patterns to detect potentially malicious code
712
+ DANGEROUS_PATTERNS = [
713
+ r'__import__\s*\(',
714
+ r'eval\s*\(',
715
+ r'exec\s*\(',
716
+ r'compile\s*\(',
717
+ r'os\.system\s*\(',
718
+ r'subprocess\.',
719
+ r'rm\s+-rf',
720
+ r'del\s+/\s',
721
+ ]
722
+
723
+ @staticmethod
724
+ def validate_prompt(prompt: str) -> Tuple[bool, Optional[str]]:
725
+ """
726
+ Validate input prompt for safety.
727
+
728
+ Args:
729
+ prompt: Input prompt
730
+
731
+ Returns:
732
+ Tuple of (is_valid, error_message)
733
+ """
734
+ if not prompt or not prompt.strip():
735
+ return False, "Prompt cannot be empty"
736
+
737
+ if len(prompt) > 10000:
738
+ return False, "Prompt too long (max 10000 characters)"
739
+
740
+ # Check for requests to generate malicious code
741
+ malicious_keywords = [
742
+ "malware", "virus", "exploit", "hack", "crack",
743
+ "ransomware", "keylogger", "backdoor"
744
+ ]
745
+
746
+ prompt_lower = prompt.lower()
747
+ for keyword in malicious_keywords:
748
+ if keyword in prompt_lower:
749
+ logger.warning(f"Potentially malicious prompt detected: {keyword}")
750
+ return False, f"Prompt contains potentially malicious intent: {keyword}"
751
+
752
+ return True, None
753
+
754
+ @staticmethod
755
+ def sanitize_code(code: str) -> str:
756
+ """
757
+ Sanitize generated code.
758
+
759
+ Args:
760
+ code: Generated code
761
+
762
+ Returns:
763
+ Sanitized code
764
+ """
765
+ # Remove any trailing whitespace
766
+ code = code.strip()
767
+
768
+ # Ensure proper encoding
769
+ code = code.encode('utf-8', errors='ignore').decode('utf-8')
770
+
771
+ return code
772
+
773
+ @staticmethod
774
+ def analyze_safety(code: str) -> List[str]:
775
+ """
776
+ Analyze code for safety issues.
777
+
778
+ Args:
779
+ code: Code to analyze
780
+
781
+ Returns:
782
+ List of warnings
783
+ """
784
+ import re
785
+
786
+ warnings = []
787
+
788
+ for pattern in CodeValidator.DANGEROUS_PATTERNS:
789
+ if re.search(pattern, code):
790
+ warnings.append(f"Potentially dangerous pattern detected: {pattern}")
791
+
792
+ return warnings
793
+
794
+
795
+ # ============================================================================
796
+ # CORE ENGINE
797
+ # ============================================================================
798
+
799
+ class TrovikuCore:
800
+ """
801
+ Core engine that orchestrates all Troviku functionality.
802
+ This is the central class that everything depends on.
803
+ """
804
+
805
+ def __init__(
806
+ self,
807
+ api_key: Optional[str] = None,
808
+ model_path: Optional[str] = None,
809
+ mode: InferenceMode = InferenceMode.API,
810
+ config: Optional[Dict[str, Any]] = None
811
+ ):
812
+ """
813
+ Initialize Troviku core engine.
814
+
815
+ Args:
816
+ api_key: OpenTrouter API key (required for API mode)
817
+ model_path: Path to local model (required for LOCAL mode)
818
+ mode: Inference mode (API, LOCAL, or HYBRID)
819
+ config: Custom configuration dictionary
820
+ """
821
+ logger.info("Initializing Troviku Core Engine v1.1.0")
822
+
823
+ # Merge custom config with defaults
824
+ self.config = {**TROVIKU_CONFIG, **(config or {})}
825
+
826
+ # Set inference mode
827
+ self.mode = mode
828
+
829
+ # Initialize components
830
+ self.cache = LRUCache(max_size=self.config["cache_size"]) if self.config["cache_enabled"] else None
831
+ self.rate_limiter = RateLimiter(requests_per_minute=self.config["rate_limit_per_minute"])
832
+ self.validator = CodeValidator()
833
+
834
+ # Initialize API client if needed
835
+ self.api_client = None
836
+ if mode in [InferenceMode.API, InferenceMode.HYBRID]:
837
+ if not api_key:
838
+ api_key = os.getenv("TROVIKU_API_KEY") or os.getenv("OPENTROUTER_API_KEY")
839
+ if not api_key:
840
+ raise AuthenticationException("API key required for API mode")
841
+ self.api_client = APIClient(
842
+ api_key=api_key,
843
+ base_url=self.config["api_base_url"],
844
+ timeout=self.config["default_timeout"],
845
+ max_retries=self.config["max_retries"]
846
+ )
847
+
848
+ # Initialize model loader if needed
849
+ self.model_loader = None
850
+ if mode in [InferenceMode.LOCAL, InferenceMode.HYBRID]:
851
+ self.model_loader = ModelLoader(model_path=model_path)
852
+ if model_path:
853
+ self.model_loader.load_model()
854
+
855
+ # Statistics
856
+ self.stats = {
857
+ "requests": 0,
858
+ "successes": 0,
859
+ "failures": 0,
860
+ "total_tokens": 0,
861
+ "total_time": 0.0
862
+ }
863
+
864
+ logger.info(f"Troviku Core initialized in {mode.value} mode")
865
+
866
+ def generate(
867
+ self,
868
+ prompt: str,
869
+ language: Optional[Union[str, Language]] = None,
870
+ task_type: TaskType = TaskType.GENERATION,
871
+ config: Optional[GenerationConfig] = None,
872
+ context: Optional[str] = None,
873
+ use_cache: bool = True
874
+ ) -> CodeResponse:
875
+ """
876
+ Generate code based on prompt.
877
+
878
+ This is the main entry point for code generation.
879
+
880
+ Args:
881
+ prompt: Description or partial code
882
+ language: Target programming language
883
+ task_type: Type of task to perform
884
+ config: Generation configuration
885
+ context: Additional context
886
+ use_cache: Whether to use cached results
887
+
888
+ Returns:
889
+ CodeResponse with generated code
890
+
891
+ Raises:
892
+ ValidationException: If input is invalid
893
+ APIException: If API request fails
894
+ ModelException: If local inference fails
895
+ """
896
+ start_time = time.time()
897
+ self.stats["requests"] += 1
898
+
899
+ # Create request object
900
+ request = CodeRequest(
901
+ prompt=prompt,
902
+ language=language,
903
+ task_type=task_type,
904
+ config=config or GenerationConfig(),
905
+ context=context
906
+ )
907
+
908
+ # Validate prompt
909
+ is_valid, error = self.validator.validate_prompt(prompt)
910
+ if not is_valid:
911
+ raise ValidationException(error)
912
+
913
+ # Check cache
914
+ if use_cache and self.cache:
915
+ cached_response = self.cache.get(request)
916
+ if cached_response:
917
+ logger.info("Returning cached response")
918
+ return cached_response
919
+
920
+ # Wait for rate limit
921
+ self.rate_limiter.wait_for_token()
922
+
923
+ # Generate code
924
+ try:
925
+ if self.mode == InferenceMode.API:
926
+ response = self._generate_api(request)
927
+ elif self.mode == InferenceMode.LOCAL:
928
+ response = self._generate_local(request)
929
+ elif self.mode == InferenceMode.HYBRID:
930
+ try:
931
+ response = self._generate_api(request)
932
+ except Exception as e:
933
+ logger.warning(f"API failed, falling back to local: {e}")
934
+ response = self._generate_local(request)
935
+
936
+ # Validate and sanitize response
937
+ response.code = self.validator.sanitize_code(response.code)
938
+
939
+ if self.config["validate_responses"]:
940
+ warnings = self.validator.analyze_safety(response.code)
941
+ response.warnings = warnings
942
+
943
+ # Calculate execution time
944
+ response.execution_time = time.time() - start_time
945
+
946
+ # Cache response
947
+ if use_cache and self.cache:
948
+ self.cache.put(request, response)
949
+
950
+ # Update statistics
951
+ self.stats["successes"] += 1
952
+ self.stats["total_tokens"] += response.tokens_used
953
+ self.stats["total_time"] += response.execution_time
954
+
955
+ logger.info(f"Generated code in {response.execution_time:.2f}s")
956
+ return response
957
+
958
+ except Exception as e:
959
+ self.stats["failures"] += 1
960
+ logger.error(f"Generation failed: {str(e)}")
961
+ raise
962
+
963
+ def _generate_api(self, request: CodeRequest) -> CodeResponse:
964
+ """Generate using API."""
965
+ if not self.api_client:
966
+ raise APIException("API client not initialized")
967
+
968
+ # Prepare API payload
969
+ messages = []
970
+
971
+ if request.context:
972
+ messages.append({"role": "system", "content": request.context})
973
+
974
+ if request.language:
975
+ messages.append({
976
+ "role": "system",
977
+ "content": f"Generate {request.language} code for the following task."
978
+ })
979
+
980
+ messages.append({"role": "user", "content": request.prompt})
981
+
982
+ payload = {
983
+ "model": self.config["default_model"],
984
+ "messages": messages,
985
+ **request.config.to_dict()
986
+ }
987
+
988
+ # Make API request
989
+ api_response = self.api_client.make_request("/chat/completions", payload)
990
+
991
+ # Parse response
992
+ content = api_response["choices"][0]["message"]["content"]
993
+ usage = api_response.get("usage", {})
994
+
995
+ return CodeResponse(
996
+ code=content,
997
+ language=request.language or "auto",
998
+ task_type=request.task_type.value,
999
+ confidence=0.95, # API doesn't provide confidence
1000
+ tokens_used=usage.get("total_tokens", 0),
1001
+ execution_time=0.0, # Will be set by caller
1002
+ model_version=self.config["default_model"],
1003
+ cached=False
1004
+ )
1005
+
1006
+ def _generate_local(self, request: CodeRequest) -> CodeResponse:
1007
+ """Generate using local model."""
1008
+ if not self.model_loader or not self.model_loader.loaded:
1009
+ raise ModelException("Local model not loaded")
1010
+
1011
+ code = self.model_loader.generate(request.prompt, request.config)
1012
+
1013
+ return CodeResponse(
1014
+ code=code,
1015
+ language=request.language or "auto",
1016
+ task_type=request.task_type.value,
1017
+ confidence=0.85,
1018
+ tokens_used=len(code.split()), # Rough estimate
1019
+ execution_time=0.0,
1020
+ model_version="1.1.0-local",
1021
+ cached=False
1022
+ )
1023
+
1024
+ def get_statistics(self) -> Dict[str, Any]:
1025
+ """
1026
+ Get engine statistics.
1027
+
1028
+ Returns:
1029
+ Dictionary with statistics
1030
+ """
1031
+ stats = self.stats.copy()
1032
+
1033
+ if stats["requests"] > 0:
1034
+ stats["success_rate"] = stats["successes"] / stats["requests"]
1035
+ stats["average_tokens"] = stats["total_tokens"] / stats["successes"] if stats["successes"] > 0 else 0
1036
+ stats["average_time"] = stats["total_time"] / stats["successes"] if stats["successes"] > 0 else 0
1037
+
1038
+ if self.cache:
1039
+ stats["cache"] = self.cache.stats()
1040
+
1041
+ return stats
1042
+
1043
+ def reset_statistics(self):
1044
+ """Reset all statistics."""
1045
+ self.stats = {
1046
+ "requests": 0,
1047
+ "successes": 0,
1048
+ "failures": 0,
1049
+ "total_tokens": 0,
1050
+ "total_time": 0.0
1051
+ }
1052
+ if self.cache:
1053
+ self.cache.clear()
1054
+ logger.info("Statistics reset")
1055
+
1056
+ def health_check(self) -> Dict[str, Any]:
1057
+ """
1058
+ Perform health check on all components.
1059
+
1060
+ Returns:
1061
+ Health status dictionary
1062
+ """
1063
+ health = {
1064
+ "status": "healthy",
1065
+ "components": {},
1066
+ "timestamp": time.time()
1067
+ }
1068
+
1069
+ # Check API client
1070
+ if self.api_client:
1071
+ try:
1072
+ # Simple connectivity check
1073
+ health["components"]["api"] = "healthy"
1074
+ except Exception as e:
1075
+ health["components"]["api"] = f"unhealthy: {str(e)}"
1076
+ health["status"] = "degraded"
1077
+
1078
+ # Check model loader
1079
+ if self.model_loader:
1080
+ health["components"]["model"] = "healthy" if self.model_loader.loaded else "not_loaded"
1081
+
1082
+ # Check cache
1083
+ if self.cache:
1084
+ cache_stats = self.cache.stats()
1085
+ health["components"]["cache"] = {
1086
+ "status": "healthy",
1087
+ "size": cache_stats["size"],
1088
+ "hit_rate": cache_stats["hit_rate"]
1089
+ }
1090
+
1091
+ return health
1092
+
1093
+ def shutdown(self):
1094
+ """
1095
+ Gracefully shutdown the engine.
1096
+ """
1097
+ logger.info("Shutting down Troviku Core")
1098
+
1099
+ if self.api_client:
1100
+ self.api_client.close()
1101
+
1102
+ if self.model_loader:
1103
+ self.model_loader.unload()
1104
+
1105
+ if self.cache:
1106
+ self.cache.clear()
1107
+
1108
+ logger.info("Troviku Core shutdown complete")
1109
+
1110
+ def __enter__(self):
1111
+ """Context manager entry."""
1112
+ return self
1113
+
1114
+ def __exit__(self, exc_type, exc_val, exc_tb):
1115
+ """Context manager exit."""
1116
+ self.shutdown()
1117
+
1118
+
1119
+ # ============================================================================
1120
+ # UTILITY FUNCTIONS
1121
+ # ============================================================================
1122
+
1123
+ def detect_language(code: str) -> str:
1124
+ """
1125
+ Detect programming language from code snippet.
1126
+
1127
+ Args:
1128
+ code: Code snippet
1129
+
1130
+ Returns:
1131
+ Detected language name
1132
+ """
1133
+ # Simple heuristic-based detection
1134
+ indicators = {
1135
+ 'python': ['def ', 'import ', 'class ', 'print(', 'self.'],
1136
+ 'javascript': ['function ', 'const ', 'let ', 'var ', '=>', 'console.log'],
1137
+ 'java': ['public class', 'private ', 'public static void main'],
1138
+ 'cpp': ['#include', 'std::', 'cout', 'namespace'],
1139
+ 'rust': ['fn ', 'let mut', 'impl ', 'use '],
1140
+ 'go': ['func ', 'package ', 'import ('],
1141
+ 'ruby': ['def ', 'end', 'puts ', 'require '],
1142
+ 'php': ['<?php', ', 'function ', 'echo '],
1143
+ }
1144
+
1145
+ code_lower = code.lower()
1146
+ scores = {}
1147
+
1148
+ for lang, keywords in indicators.items():
1149
+ score = sum(1 for keyword in keywords if keyword.lower() in code_lower)
1150
+ if score > 0:
1151
+ scores[lang] = score
1152
+
1153
+ if scores:
1154
+ return max(scores, key=scores.get)
1155
+ return 'unknown'
1156
+
1157
+
1158
+ def format_code(code: str, language: str) -> str:
1159
+ """
1160
+ Format code with basic indentation.
1161
+
1162
+ Args:
1163
+ code: Code to format
1164
+ language: Programming language
1165
+
1166
+ Returns:
1167
+ Formatted code
1168
+ """
1169
+ # Basic formatting - in production use language-specific formatters
1170
+ lines = code.split('\n')
1171
+ formatted_lines = []
1172
+ indent_level = 0
1173
+
1174
+ for line in lines:
1175
+ stripped = line.strip()
1176
+
1177
+ # Decrease indent for closing braces/keywords
1178
+ if stripped.startswith('}') or stripped.startswith('end') or stripped == ']' or stripped == ')':
1179
+ indent_level = max(0, indent_level - 1)
1180
+
1181
+ # Add indentation
1182
+ formatted_lines.append(' ' * indent_level + stripped)
1183
+
1184
+ # Increase indent after opening braces/keywords
1185
+ if stripped.endswith('{') or stripped.endswith(':') or stripped.startswith('def ') or stripped.startswith('class '):
1186
+ indent_level += 1
1187
+
1188
+ return '\n'.join(formatted_lines)
1189
+
1190
+
1191
+ def extract_code_blocks(text: str) -> List[Tuple[str, str]]:
1192
+ """
1193
+ Extract code blocks from markdown text.
1194
+
1195
+ Args:
1196
+ text: Text containing code blocks
1197
+
1198
+ Returns:
1199
+ List of (language, code) tuples
1200
+ """
1201
+ import re
1202
+
1203
+ # Match markdown code blocks
1204
+ pattern = r'```(\w+)?\n(.*?)```'
1205
+ matches = re.findall(pattern, text, re.DOTALL)
1206
+
1207
+ return [(lang or 'unknown', code.strip()) for lang, code in matches]
1208
+
1209
+
1210
+ def calculate_code_metrics(code: str) -> Dict[str, Any]:
1211
+ """
1212
+ Calculate basic code metrics.
1213
+
1214
+ Args:
1215
+ code: Code to analyze
1216
+
1217
+ Returns:
1218
+ Dictionary with metrics
1219
+ """
1220
+ lines = code.split('\n')
1221
+
1222
+ return {
1223
+ 'total_lines': len(lines),
1224
+ 'non_empty_lines': sum(1 for line in lines if line.strip()),
1225
+ 'comment_lines': sum(1 for line in lines if line.strip().startswith('#') or line.strip().startswith('//')),
1226
+ 'total_characters': len(code),
1227
+ 'average_line_length': sum(len(line) for line in lines) / len(lines) if lines else 0
1228
+ }
1229
+
1230
+
1231
+ def validate_syntax(code: str, language: str) -> Tuple[bool, Optional[str]]:
1232
+ """
1233
+ Validate code syntax (basic check).
1234
+
1235
+ Args:
1236
+ code: Code to validate
1237
+ language: Programming language
1238
+
1239
+ Returns:
1240
+ Tuple of (is_valid, error_message)
1241
+ """
1242
+ if language == 'python':
1243
+ try:
1244
+ import ast
1245
+ ast.parse(code)
1246
+ return True, None
1247
+ except SyntaxError as e:
1248
+ return False, str(e)
1249
+
1250
+ # For other languages, just check basic structure
1251
+ if not code.strip():
1252
+ return False, "Empty code"
1253
+
1254
+ return True, None
1255
+
1256
+
1257
+ # ============================================================================
1258
+ # BATCH PROCESSING
1259
+ # ============================================================================
1260
+
1261
+ class BatchProcessor:
1262
+ """
1263
+ Process multiple code generation requests in batch.
1264
+ """
1265
+
1266
+ def __init__(self, core: TrovikuCore, max_workers: int = 4):
1267
+ """
1268
+ Initialize batch processor.
1269
+
1270
+ Args:
1271
+ core: TrovikuCore instance
1272
+ max_workers: Maximum parallel workers
1273
+ """
1274
+ self.core = core
1275
+ self.max_workers = max_workers
1276
+
1277
+ def process_batch(
1278
+ self,
1279
+ requests: List[Dict[str, Any]],
1280
+ show_progress: bool = True
1281
+ ) -> List[CodeResponse]:
1282
+ """
1283
+ Process multiple requests.
1284
+
1285
+ Args:
1286
+ requests: List of request dictionaries
1287
+ show_progress: Whether to show progress
1288
+
1289
+ Returns:
1290
+ List of CodeResponse objects
1291
+ """
1292
+ import concurrent.futures
1293
+
1294
+ results = []
1295
+ total = len(requests)
1296
+
1297
+ with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
1298
+ futures = []
1299
+
1300
+ for req in requests:
1301
+ future = executor.submit(
1302
+ self.core.generate,
1303
+ prompt=req['prompt'],
1304
+ language=req.get('language'),
1305
+ task_type=req.get('task_type', TaskType.GENERATION),
1306
+ config=req.get('config')
1307
+ )
1308
+ futures.append(future)
1309
+
1310
+ for i, future in enumerate(concurrent.futures.as_completed(futures), 1):
1311
+ try:
1312
+ result = future.result()
1313
+ results.append(result)
1314
+ if show_progress:
1315
+ logger.info(f"Progress: {i}/{total}")
1316
+ except Exception as e:
1317
+ logger.error(f"Request failed: {str(e)}")
1318
+ results.append(None)
1319
+
1320
+ return results
1321
+
1322
+
1323
+ # ============================================================================
1324
+ # CONFIGURATION MANAGEMENT
1325
+ # ============================================================================
1326
+
1327
+ class ConfigManager:
1328
+ """
1329
+ Manage Troviku configuration files.
1330
+ """
1331
+
1332
+ DEFAULT_CONFIG_PATH = Path.home() / ".troviku" / "config.json"
1333
+
1334
+ @staticmethod
1335
+ def load_config(path: Optional[Path] = None) -> Dict[str, Any]:
1336
+ """
1337
+ Load configuration from file.
1338
+
1339
+ Args:
1340
+ path: Configuration file path
1341
+
1342
+ Returns:
1343
+ Configuration dictionary
1344
+ """
1345
+ config_path = path or ConfigManager.DEFAULT_CONFIG_PATH
1346
+
1347
+ if config_path.exists():
1348
+ try:
1349
+ with open(config_path, 'r') as f:
1350
+ config = json.load(f)
1351
+ logger.info(f"Configuration loaded from {config_path}")
1352
+ return config
1353
+ except Exception as e:
1354
+ logger.error(f"Failed to load config: {e}")
1355
+ return TROVIKU_CONFIG.copy()
1356
+
1357
+ return TROVIKU_CONFIG.copy()
1358
+
1359
+ @staticmethod
1360
+ def save_config(config: Dict[str, Any], path: Optional[Path] = None):
1361
+ """
1362
+ Save configuration to file.
1363
+
1364
+ Args:
1365
+ config: Configuration dictionary
1366
+ path: Configuration file path
1367
+ """
1368
+ config_path = path or ConfigManager.DEFAULT_CONFIG_PATH
1369
+ config_path.parent.mkdir(parents=True, exist_ok=True)
1370
+
1371
+ try:
1372
+ with open(config_path, 'w') as f:
1373
+ json.dump(config, f, indent=2)
1374
+ logger.info(f"Configuration saved to {config_path}")
1375
+ except Exception as e:
1376
+ logger.error(f"Failed to save config: {e}")
1377
+
1378
+ @staticmethod
1379
+ def get_api_key() -> Optional[str]:
1380
+ """
1381
+ Get API key from environment or config.
1382
+
1383
+ Returns:
1384
+ API key if found
1385
+ """
1386
+ # Check environment variables
1387
+ api_key = os.getenv("TROVIKU_API_KEY") or os.getenv("OPENTROUTER_API_KEY")
1388
+
1389
+ if api_key:
1390
+ return api_key
1391
+
1392
+ # Check config file
1393
+ config = ConfigManager.load_config()
1394
+ return config.get("api_key")
1395
+
1396
+
1397
+ # ============================================================================
1398
+ # PLUGIN SYSTEM
1399
+ # ============================================================================
1400
+
1401
+ class Plugin:
1402
+ """
1403
+ Base class for Troviku plugins.
1404
+ """
1405
+
1406
+ def __init__(self, name: str):
1407
+ """
1408
+ Initialize plugin.
1409
+
1410
+ Args:
1411
+ name: Plugin name
1412
+ """
1413
+ self.name = name
1414
+ self.enabled = True
1415
+
1416
+ def pre_generate(self, request: CodeRequest) -> CodeRequest:
1417
+ """
1418
+ Hook called before generation.
1419
+
1420
+ Args:
1421
+ request: Code request
1422
+
1423
+ Returns:
1424
+ Modified request
1425
+ """
1426
+ return request
1427
+
1428
+ def post_generate(self, response: CodeResponse) -> CodeResponse:
1429
+ """
1430
+ Hook called after generation.
1431
+
1432
+ Args:
1433
+ response: Code response
1434
+
1435
+ Returns:
1436
+ Modified response
1437
+ """
1438
+ return response
1439
+
1440
+
1441
+ class PluginManager:
1442
+ """
1443
+ Manage Troviku plugins.
1444
+ """
1445
+
1446
+ def __init__(self):
1447
+ """Initialize plugin manager."""
1448
+ self.plugins: List[Plugin] = []
1449
+
1450
+ def register(self, plugin: Plugin):
1451
+ """
1452
+ Register a plugin.
1453
+
1454
+ Args:
1455
+ plugin: Plugin instance
1456
+ """
1457
+ self.plugins.append(plugin)
1458
+ logger.info(f"Plugin registered: {plugin.name}")
1459
+
1460
+ def unregister(self, plugin_name: str):
1461
+ """
1462
+ Unregister a plugin.
1463
+
1464
+ Args:
1465
+ plugin_name: Name of plugin to remove
1466
+ """
1467
+ self.plugins = [p for p in self.plugins if p.name != plugin_name]
1468
+ logger.info(f"Plugin unregistered: {plugin_name}")
1469
+
1470
+ def execute_pre_hooks(self, request: CodeRequest) -> CodeRequest:
1471
+ """Execute all pre-generation hooks."""
1472
+ for plugin in self.plugins:
1473
+ if plugin.enabled:
1474
+ request = plugin.pre_generate(request)
1475
+ return request
1476
+
1477
+ def execute_post_hooks(self, response: CodeResponse) -> CodeResponse:
1478
+ """Execute all post-generation hooks."""
1479
+ for plugin in self.plugins:
1480
+ if plugin.enabled:
1481
+ response = plugin.post_generate(response)
1482
+ return response
1483
+
1484
+
1485
+ # ============================================================================
1486
+ # EXPORT UTILITIES
1487
+ # ============================================================================
1488
+
1489
+ class ExportManager:
1490
+ """
1491
+ Export generated code to various formats.
1492
+ """
1493
+
1494
+ @staticmethod
1495
+ def to_file(response: CodeResponse, filepath: str, include_metadata: bool = True):
1496
+ """
1497
+ Export code to file.
1498
+
1499
+ Args:
1500
+ response: Code response
1501
+ filepath: Output file path
1502
+ include_metadata: Whether to include metadata as comments
1503
+ """
1504
+ path = Path(filepath)
1505
+ path.parent.mkdir(parents=True, exist_ok=True)
1506
+
1507
+ content = []
1508
+
1509
+ if include_metadata:
1510
+ content.append(f"# Generated by Troviku {response.model_version}")
1511
+ content.append(f"# Language: {response.language}")
1512
+ content.append(f"# Task: {response.task_type}")
1513
+ content.append(f"# Tokens: {response.tokens_used}")
1514
+ content.append(f"# Time: {response.execution_time:.2f}s")
1515
+ content.append("")
1516
+
1517
+ content.append(response.code)
1518
+
1519
+ with open(path, 'w', encoding='utf-8') as f:
1520
+ f.write('\n'.join(content))
1521
+
1522
+ logger.info(f"Code exported to {filepath}")
1523
+
1524
+ @staticmethod
1525
+ def to_json(response: CodeResponse, filepath: str):
1526
+ """
1527
+ Export response to JSON.
1528
+
1529
+ Args:
1530
+ response: Code response
1531
+ filepath: Output file path
1532
+ """
1533
+ with open(filepath, 'w', encoding='utf-8') as f:
1534
+ f.write(response.to_json())
1535
+
1536
+ logger.info(f"Response exported to {filepath}")
1537
+
1538
+ @staticmethod
1539
+ def to_markdown(response: CodeResponse, filepath: str):
1540
+ """
1541
+ Export code as markdown document.
1542
+
1543
+ Args:
1544
+ response: Code response
1545
+ filepath: Output file path
1546
+ """
1547
+ md_content = f"""# Generated Code
1548
+
1549
+ **Language:** {response.language}
1550
+ **Task Type:** {response.task_type}
1551
+ **Model:** {response.model_version}
1552
+ **Tokens Used:** {response.tokens_used}
1553
+ **Execution Time:** {response.execution_time:.2f}s
1554
+
1555
+ ## Code
1556
+
1557
+ ```{response.language}
1558
+ {response.code}
1559
+ ```
1560
+
1561
+ ## Metrics
1562
+
1563
+ - Lines: {len(response.code.split('\n'))}
1564
+ - Characters: {len(response.code)}
1565
+ - Confidence: {response.confidence:.2%}
1566
+ """
1567
+
1568
+ if response.warnings:
1569
+ md_content += "\n## Warnings\n\n"
1570
+ for warning in response.warnings:
1571
+ md_content += f"- {warning}\n"
1572
+
1573
+ with open(filepath, 'w', encoding='utf-8') as f:
1574
+ f.write(md_content)
1575
+
1576
+ logger.info(f"Markdown exported to {filepath}")
1577
+
1578
+
1579
+ # ============================================================================
1580
+ # MONITORING AND TELEMETRY
1581
+ # ============================================================================
1582
+
1583
+ class TelemetryCollector:
1584
+ """
1585
+ Collect telemetry data for monitoring and analysis.
1586
+ """
1587
+
1588
+ def __init__(self, enabled: bool = True):
1589
+ """
1590
+ Initialize telemetry collector.
1591
+
1592
+ Args:
1593
+ enabled: Whether telemetry is enabled
1594
+ """
1595
+ self.enabled = enabled
1596
+ self.events: List[Dict[str, Any]] = []
1597
+
1598
+ def record_event(
1599
+ self,
1600
+ event_type: str,
1601
+ data: Dict[str, Any],
1602
+ timestamp: Optional[float] = None
1603
+ ):
1604
+ """
1605
+ Record a telemetry event.
1606
+
1607
+ Args:
1608
+ event_type: Type of event
1609
+ data: Event data
1610
+ timestamp: Event timestamp (default: current time)
1611
+ """
1612
+ if not self.enabled:
1613
+ return
1614
+
1615
+ event = {
1616
+ "type": event_type,
1617
+ "timestamp": timestamp or time.time(),
1618
+ "data": data
1619
+ }
1620
+
1621
+ self.events.append(event)
1622
+
1623
+ def get_events(
1624
+ self,
1625
+ event_type: Optional[str] = None,
1626
+ limit: Optional[int] = None
1627
+ ) -> List[Dict[str, Any]]:
1628
+ """
1629
+ Get recorded events.
1630
+
1631
+ Args:
1632
+ event_type: Filter by event type
1633
+ limit: Maximum number of events to return
1634
+
1635
+ Returns:
1636
+ List of events
1637
+ """
1638
+ events = self.events
1639
+
1640
+ if event_type:
1641
+ events = [e for e in events if e["type"] == event_type]
1642
+
1643
+ if limit:
1644
+ events = events[-limit:]
1645
+
1646
+ return events
1647
+
1648
+ def clear_events(self):
1649
+ """Clear all recorded events."""
1650
+ self.events.clear()
1651
+
1652
+ def export_events(self, filepath: str):
1653
+ """
1654
+ Export events to JSON file.
1655
+
1656
+ Args:
1657
+ filepath: Output file path
1658
+ """
1659
+ with open(filepath, 'w', encoding='utf-8') as f:
1660
+ json.dump(self.events, f, indent=2)
1661
+
1662
+ logger.info(f"Telemetry exported to {filepath}")
1663
+
1664
+
1665
+ # ============================================================================
1666
+ # MAIN INTERFACE
1667
+ # ============================================================================
1668
+
1669
+ # Singleton instance for convenience
1670
+ _default_core: Optional[TrovikuCore] = None
1671
+
1672
+
1673
+ def initialize(
1674
+ api_key: Optional[str] = None,
1675
+ mode: InferenceMode = InferenceMode.API,
1676
+ config: Optional[Dict[str, Any]] = None
1677
+ ) -> TrovikuCore:
1678
+ """
1679
+ Initialize the default Troviku core instance.
1680
+
1681
+ Args:
1682
+ api_key: OpenTrouter API key
1683
+ mode: Inference mode
1684
+ config: Custom configuration
1685
+
1686
+ Returns:
1687
+ TrovikuCore instance
1688
+ """
1689
+ global _default_core
1690
+
1691
+ if _default_core is not None:
1692
+ logger.warning("Troviku already initialized, returning existing instance")
1693
+ return _default_core
1694
+
1695
+ _default_core = TrovikuCore(api_key=api_key, mode=mode, config=config)
1696
+ return _default_core
1697
+
1698
+
1699
+ def get_core() -> TrovikuCore:
1700
+ """
1701
+ Get the default core instance.
1702
+
1703
+ Returns:
1704
+ TrovikuCore instance
1705
+
1706
+ Raises:
1707
+ RuntimeError: If core not initialized
1708
+ """
1709
+ if _default_core is None:
1710
+ raise RuntimeError("Troviku not initialized. Call initialize() first.")
1711
+ return _default_core
1712
+
1713
+
1714
+ def generate(prompt: str, **kwargs) -> CodeResponse:
1715
+ """
1716
+ Convenience function for code generation.
1717
+
1718
+ Args:
1719
+ prompt: Code generation prompt
1720
+ **kwargs: Additional arguments for generate()
1721
+
1722
+ Returns:
1723
+ CodeResponse
1724
+ """
1725
+ return get_core().generate(prompt, **kwargs)
1726
+
1727
+
1728
+ def shutdown():
1729
+ """Shutdown the default core instance."""
1730
+ global _default_core
1731
+ if _default_core:
1732
+ _default_core.shutdown()
1733
+ _default_core = None
1734
+
1735
+
1736
+ # ============================================================================
1737
+ # EXAMPLE USAGE
1738
+ # ============================================================================
1739
+
1740
+ if __name__ == "__main__":
1741
+ # Example 1: Basic usage
1742
+ print("="*60)
1743
+ print("Example 1: Basic Code Generation")
1744
+ print("="*60)
1745
+
1746
+ try:
1747
+ # Initialize core
1748
+ core = initialize(api_key="your_api_key_here", mode=InferenceMode.API)
1749
+
1750
+ # Generate code
1751
+ response = core.generate(
1752
+ prompt="Create a function to calculate the factorial of a number",
1753
+ language=Language.PYTHON
1754
+ )
1755
+
1756
+ print(f"Generated Code:\n{response.code}")
1757
+ print(f"\nTokens used: {response.tokens_used}")
1758
+ print(f"Execution time: {response.execution_time:.2f}s")
1759
+
1760
+ # Get statistics
1761
+ stats = core.get_statistics()
1762
+ print(f"\nStatistics: {json.dumps(stats, indent=2)}")
1763
+
1764
+ except Exception as e:
1765
+ logger.error(f"Error: {e}")
1766
+ finally:
1767
+ shutdown()
1768
+
1769
+ print("\n" + "="*60)
1770
+ print("Example 2: Batch Processing")
1771
+ print("="*60)
1772
+
1773
+ try:
1774
+ core = initialize(api_key="your_api_key_here")
1775
+ processor = BatchProcessor(core, max_workers=2)
1776
+
1777
+ requests = [
1778
+ {"prompt": "Sort a list of numbers", "language": "python"},
1779
+ {"prompt": "Reverse a string", "language": "python"},
1780
+ {"prompt": "Find the maximum in an array", "language": "javascript"}
1781
+ ]
1782
+
1783
+ results = processor.process_batch(requests)
1784
+
1785
+ for i, result in enumerate(results, 1):
1786
+ if result:
1787
+ print(f"\nRequest {i}:")
1788
+ print(result.code[:200] + "...")
1789
+
1790
+ except Exception as e:
1791
+ logger.error(f"Error: {e}")
1792
+ finally:
1793
+ shutdown()
1794
+
1795
+
1796
+ # ============================================================================
1797
+ # VERSION INFO
1798
+ # ============================================================================
1799
+
1800
+ __version__ = "1.1.0"
1801
+ __author__ = "OpenTrouter Research Team"
1802
+ __license__ = "Apache 2.0"
1803
+ __all__ = [
1804
+ # Main classes
1805
+ "TrovikuCore",
1806
+ "CodeRequest",
1807
+ "CodeResponse",
1808
+ "GenerationConfig",
1809
+
1810
+ # Enums
1811
+ "Language",
1812
+ "TaskType",
1813
+ "InferenceMode",
1814
+ "ModelVersion",
1815
+
1816
+ # Utilities
1817
+ "detect_language",
1818
+ "format_code",
1819
+ "calculate_code_metrics",
1820
+ "validate_syntax",
1821
+
1822
+ # Management
1823
+ "ConfigManager",
1824
+ "ExportManager",
1825
+ "BatchProcessor",
1826
+ "PluginManager",
1827
+
1828
+ # Convenience functions
1829
+ "initialize",
1830
+ "get_core",
1831
+ "generate",
1832
+ "shutdown",
1833
+
1834
+ # Exceptions
1835
+ "TrovikuException",
1836
+ "APIException",
1837
+ "ValidationException",
1838
+ "ModelException",
1839
+ ]