empathy-framework 3.7.1__py3-none-any.whl → 3.8.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.
@@ -28,6 +28,7 @@ Licensed under Fair Source 0.9
28
28
  """
29
29
 
30
30
  import base64
31
+ import binascii
31
32
  import concurrent.futures
32
33
  import hashlib
33
34
  import json
@@ -168,7 +169,7 @@ class EncryptionManager:
168
169
  if env_key := os.getenv("EMPATHY_MASTER_KEY"):
169
170
  try:
170
171
  return base64.b64decode(env_key)
171
- except Exception as e:
172
+ except (binascii.Error, ValueError) as e:
172
173
  logger.error("invalid_master_key_in_env", error=str(e))
173
174
  raise ValueError("Invalid EMPATHY_MASTER_KEY format") from e
174
175
 
@@ -177,7 +178,7 @@ class EncryptionManager:
177
178
  if key_file.exists():
178
179
  try:
179
180
  return key_file.read_bytes()
180
- except Exception as e:
181
+ except (OSError, PermissionError) as e:
181
182
  logger.error("failed_to_load_key_file", error=str(e))
182
183
 
183
184
  # Generate ephemeral key (NOT for production)
@@ -219,7 +220,7 @@ class EncryptionManager:
219
220
  # Return base64-encoded
220
221
  return base64.b64encode(encrypted_data).decode("utf-8")
221
222
 
222
- except Exception as e:
223
+ except (ValueError, TypeError, UnicodeEncodeError) as e:
223
224
  logger.error("encryption_failed", error=str(e))
224
225
  raise SecurityError(f"Encryption failed: {e}") from e
225
226
 
@@ -255,7 +256,7 @@ class EncryptionManager:
255
256
 
256
257
  return plaintext_bytes.decode("utf-8")
257
258
 
258
- except Exception as e:
259
+ except (ValueError, TypeError, UnicodeDecodeError, binascii.Error) as e:
259
260
  logger.error("decryption_failed", error=str(e))
260
261
  raise SecurityError(f"Decryption failed: {e}") from e
261
262
 
@@ -307,7 +308,7 @@ class MemDocsStorage:
307
308
  logger.debug("pattern_stored", pattern_id=pattern_id)
308
309
  return True
309
310
 
310
- except Exception as e:
311
+ except (OSError, PermissionError, json.JSONDecodeError) as e:
311
312
  logger.error("pattern_storage_failed", pattern_id=pattern_id, error=str(e))
312
313
  raise
313
314
 
@@ -334,7 +335,7 @@ class MemDocsStorage:
334
335
  logger.debug("pattern_retrieved", pattern_id=pattern_id)
335
336
  return pattern_data
336
337
 
337
- except Exception as e:
338
+ except (OSError, PermissionError, json.JSONDecodeError) as e:
338
339
  logger.error("pattern_retrieval_failed", pattern_id=pattern_id, error=str(e))
339
340
  return None
340
341
 
@@ -358,7 +359,7 @@ class MemDocsStorage:
358
359
  logger.info("pattern_deleted", pattern_id=pattern_id)
359
360
  return True
360
361
 
361
- except Exception as e:
362
+ except (OSError, PermissionError) as e:
362
363
  logger.error("pattern_deletion_failed", pattern_id=pattern_id, error=str(e))
363
364
  return False
364
365
 
@@ -33,6 +33,8 @@ try:
33
33
  except ImportError:
34
34
  pass # python-dotenv not installed, rely on environment variables
35
35
 
36
+ # Import caching infrastructure
37
+ from empathy_os.cache import BaseCache, auto_setup_cache, create_cache
36
38
  from empathy_os.cost_tracker import MODEL_PRICING, CostTracker
37
39
 
38
40
  # Import unified types from empathy_os.models
@@ -160,6 +162,12 @@ class CostReport:
160
162
  savings_percent: float
161
163
  by_stage: dict[str, float] = field(default_factory=dict)
162
164
  by_tier: dict[str, float] = field(default_factory=dict)
165
+ # Cache metrics
166
+ cache_hits: int = 0
167
+ cache_misses: int = 0
168
+ cache_hit_rate: float = 0.0
169
+ estimated_cost_without_cache: float = 0.0
170
+ savings_from_cache: float = 0.0
163
171
 
164
172
 
165
173
  @dataclass
@@ -366,6 +374,8 @@ class BaseWorkflow(ABC):
366
374
  executor: LLMExecutor | None = None,
367
375
  telemetry_backend: TelemetryBackend | None = None,
368
376
  progress_callback: ProgressCallback | None = None,
377
+ cache: BaseCache | None = None,
378
+ enable_cache: bool = True,
369
379
  ):
370
380
  """Initialize workflow with optional cost tracker, provider, and config.
371
381
 
@@ -381,6 +391,9 @@ class BaseWorkflow(ABC):
381
391
  Defaults to TelemetryStore (JSONL file backend).
382
392
  progress_callback: Callback for real-time progress updates.
383
393
  If provided, enables live progress tracking during execution.
394
+ cache: Optional cache instance. If None and enable_cache=True,
395
+ auto-creates cache with one-time setup prompt.
396
+ enable_cache: Whether to enable caching (default True).
384
397
 
385
398
  """
386
399
  from .config import WorkflowConfig
@@ -398,6 +411,11 @@ class BaseWorkflow(ABC):
398
411
  self._run_id: str | None = None # Set at start of execute()
399
412
  self._api_key: str | None = None # For default executor creation
400
413
 
414
+ # Cache support
415
+ self._cache: BaseCache | None = cache
416
+ self._enable_cache = enable_cache
417
+ self._cache_setup_attempted = False
418
+
401
419
  # Load config if not provided
402
420
  self._config = config or WorkflowConfig.load()
403
421
 
@@ -434,23 +452,61 @@ class BaseWorkflow(ABC):
434
452
  model = get_model(provider_str, tier.value, self._config)
435
453
  return model
436
454
 
455
+ def _maybe_setup_cache(self) -> None:
456
+ """Set up cache with one-time user prompt if needed.
457
+
458
+ This is called lazily on first workflow execution to avoid
459
+ blocking workflow initialization.
460
+ """
461
+ if not self._enable_cache:
462
+ return
463
+
464
+ if self._cache_setup_attempted:
465
+ return
466
+
467
+ self._cache_setup_attempted = True
468
+
469
+ # If cache already provided, use it
470
+ if self._cache is not None:
471
+ return
472
+
473
+ # Otherwise, trigger auto-setup (which may prompt user)
474
+ try:
475
+ auto_setup_cache()
476
+ self._cache = create_cache()
477
+ logger.info(f"Cache initialized for workflow: {self.name}")
478
+ except ImportError:
479
+ # Hybrid cache dependencies not available, fall back to hash-only
480
+ logger.info(
481
+ "Using hash-only cache (install empathy-framework[cache] for semantic caching)"
482
+ )
483
+ self._cache = create_cache(cache_type="hash")
484
+ except Exception:
485
+ # Graceful degradation - disable cache if setup fails
486
+ logger.warning("Cache setup failed, continuing without cache")
487
+ self._enable_cache = False
488
+
437
489
  async def _call_llm(
438
490
  self,
439
491
  tier: ModelTier,
440
492
  system: str,
441
493
  user_message: str,
442
494
  max_tokens: int = 4096,
495
+ stage_name: str | None = None,
443
496
  ) -> tuple[str, int, int]:
444
497
  """Provider-agnostic LLM call using the configured provider.
445
498
 
446
499
  This method uses run_step_with_executor internally to make LLM calls
447
500
  that respect the configured provider (anthropic, openai, google, etc.).
448
501
 
502
+ Supports automatic caching to reduce API costs and latency.
503
+
449
504
  Args:
450
505
  tier: Model tier to use (CHEAP, CAPABLE, PREMIUM)
451
506
  system: System prompt
452
507
  user_message: User message/prompt
453
508
  max_tokens: Maximum tokens in response
509
+ stage_name: Optional stage name for cache key (defaults to tier)
454
510
 
455
511
  Returns:
456
512
  Tuple of (response_content, input_tokens, output_tokens)
@@ -458,9 +514,32 @@ class BaseWorkflow(ABC):
458
514
  """
459
515
  from .step_config import WorkflowStepConfig
460
516
 
517
+ # Determine stage name for cache key
518
+ stage = stage_name or f"llm_call_{tier.value}"
519
+ model = self.get_model_for_tier(tier)
520
+
521
+ # Try cache lookup if enabled
522
+ if self._enable_cache and self._cache is not None:
523
+ try:
524
+ # Combine system + user message for cache key
525
+ full_prompt = f"{system}\n\n{user_message}" if system else user_message
526
+ cached_response = self._cache.get(self.name, stage, full_prompt, model)
527
+
528
+ if cached_response is not None:
529
+ logger.debug(f"Cache hit for {self.name}:{stage}")
530
+ # Cached response is dict with content, input_tokens, output_tokens
531
+ return (
532
+ cached_response["content"],
533
+ cached_response["input_tokens"],
534
+ cached_response["output_tokens"],
535
+ )
536
+ except Exception:
537
+ # Cache lookup failed - continue with LLM call
538
+ logger.debug("Cache lookup failed, continuing with LLM call")
539
+
461
540
  # Create a step config for this call
462
541
  step = WorkflowStepConfig(
463
- name=f"llm_call_{tier.value}",
542
+ name=stage,
464
543
  task_type="general",
465
544
  tier_hint=tier.value,
466
545
  description="LLM call",
@@ -473,6 +552,22 @@ class BaseWorkflow(ABC):
473
552
  prompt=user_message,
474
553
  system=system,
475
554
  )
555
+
556
+ # Store in cache if enabled
557
+ if self._enable_cache and self._cache is not None:
558
+ try:
559
+ full_prompt = f"{system}\n\n{user_message}" if system else user_message
560
+ response_data = {
561
+ "content": content,
562
+ "input_tokens": in_tokens,
563
+ "output_tokens": out_tokens,
564
+ }
565
+ self._cache.put(self.name, stage, full_prompt, model, response_data)
566
+ logger.debug(f"Cached response for {self.name}:{stage}")
567
+ except Exception:
568
+ # Cache storage failed - log but continue
569
+ logger.debug("Failed to cache response")
570
+
476
571
  return content, in_tokens, out_tokens
477
572
  except (ValueError, TypeError, KeyError) as e:
478
573
  # Invalid input or configuration errors
@@ -523,6 +618,33 @@ class BaseWorkflow(ABC):
523
618
  savings = baseline_cost - total_cost
524
619
  savings_percent = (savings / baseline_cost * 100) if baseline_cost > 0 else 0.0
525
620
 
621
+ # Calculate cache metrics if cache is enabled
622
+ cache_hits = 0
623
+ cache_misses = 0
624
+ cache_hit_rate = 0.0
625
+ estimated_cost_without_cache = total_cost
626
+ savings_from_cache = 0.0
627
+
628
+ if self._cache is not None:
629
+ try:
630
+ stats = self._cache.get_stats()
631
+ cache_hits = stats.hits
632
+ cache_misses = stats.misses
633
+ cache_hit_rate = stats.hit_rate
634
+
635
+ # Estimate cost without cache (assumes cache hits would have incurred full cost)
636
+ # This is a conservative estimate
637
+ if cache_hits > 0:
638
+ # Average cost per non-cached call
639
+ avg_cost_per_call = total_cost / cache_misses if cache_misses > 0 else 0.0
640
+ # Estimated additional cost if cache hits were actual API calls
641
+ estimated_additional_cost = cache_hits * avg_cost_per_call
642
+ estimated_cost_without_cache = total_cost + estimated_additional_cost
643
+ savings_from_cache = estimated_additional_cost
644
+ except (AttributeError, TypeError):
645
+ # Cache doesn't support stats or error occurred
646
+ pass
647
+
526
648
  return CostReport(
527
649
  total_cost=total_cost,
528
650
  baseline_cost=baseline_cost,
@@ -530,6 +652,11 @@ class BaseWorkflow(ABC):
530
652
  savings_percent=savings_percent,
531
653
  by_stage=by_stage,
532
654
  by_tier=by_tier,
655
+ cache_hits=cache_hits,
656
+ cache_misses=cache_misses,
657
+ cache_hit_rate=cache_hit_rate,
658
+ estimated_cost_without_cache=estimated_cost_without_cache,
659
+ savings_from_cache=savings_from_cache,
533
660
  )
534
661
 
535
662
  @abstractmethod
@@ -576,6 +703,9 @@ class BaseWorkflow(ABC):
576
703
  WorkflowResult with stages, output, and cost report
577
704
 
578
705
  """
706
+ # Set up cache (one-time setup with user prompt if needed)
707
+ self._maybe_setup_cache()
708
+
579
709
  # Set run ID for telemetry correlation
580
710
  self._run_id = str(uuid.uuid4())
581
711
 
@@ -147,4 +147,4 @@ result = await workflow.execute(
147
147
 
148
148
  **Generated:** 2026-01-05
149
149
  **Patterns:** multi-stage
150
- **Complexity:** COMPLEX
150
+ **Complexity:** COMPLEX
@@ -482,8 +482,10 @@ Create a phased approach to reduce debt sustainably."""
482
482
  prompt=user_message,
483
483
  system=system,
484
484
  )
485
- except Exception:
486
- # Fall back to legacy _call_llm if executor fails
485
+ except (RuntimeError, ValueError, TypeError, KeyError, AttributeError) as e:
486
+ # INTENTIONAL: Graceful fallback to legacy _call_llm if executor fails
487
+ # Catches executor/API/parsing errors during new execution path
488
+ logger.warning(f"Executor failed, falling back to legacy path: {e}")
487
489
  response, input_tokens, output_tokens = await self._call_llm(
488
490
  tier,
489
491
  system or "",
@@ -573,7 +573,7 @@ class SecurityAuditWorkflow(BaseWorkflow):
573
573
  if self.use_crew_for_assessment and self._crew_available:
574
574
  target = input_data.get("path", ".")
575
575
  try:
576
- crew_report = await self._crew.audit(code=target, file_path=target)
576
+ crew_report = await self._crew.audit(target=target)
577
577
  if crew_report and crew_report.findings:
578
578
  crew_enhanced = True
579
579
  # Convert crew findings to workflow format
@@ -620,11 +620,7 @@ class SecurityAuditWorkflow(BaseWorkflow):
620
620
  "risk_level": (
621
621
  "critical"
622
622
  if risk_score >= 75
623
- else "high"
624
- if risk_score >= 50
625
- else "medium"
626
- if risk_score >= 25
627
- else "low"
623
+ else "high" if risk_score >= 50 else "medium" if risk_score >= 25 else "low"
628
624
  ),
629
625
  "severity_breakdown": severity_counts,
630
626
  "by_owasp_category": {k: len(v) for k, v in by_owasp.items()},
@@ -155,4 +155,4 @@ result = await workflow.execute(
155
155
 
156
156
  **Generated:** 2026-01-05
157
157
  **Patterns:** crew-based
158
- **Complexity:** COMPLEX
158
+ **Complexity:** COMPLEX
hot_reload/__init__.py CHANGED
@@ -38,9 +38,9 @@ from .integration import HotReloadIntegration
38
38
  from .reloader import ReloadResult, WizardReloader
39
39
  from .watcher import WizardFileWatcher
40
40
  from .websocket import (
41
- ReloadNotificationManager,
42
- create_notification_callback,
43
- get_notification_manager,
41
+ ReloadNotificationManager,
42
+ create_notification_callback,
43
+ get_notification_manager,
44
44
  )
45
45
 
46
46
  __all__ = [
@@ -41,12 +41,12 @@ class SingleStagePattern(WorkflowPattern):
41
41
  return [
42
42
  CodeSection(
43
43
  location="class_attributes",
44
- code=f''' name = "{workflow_name}"
44
+ code=f""" name = "{workflow_name}"
45
45
  description = "{description}"
46
46
  stages = ["process"]
47
47
  tier_map = {{
48
48
  "process": ModelTier.{tier},
49
- }}''',
49
+ }}""",
50
50
  priority=1,
51
51
  ),
52
52
  CodeSection(
@@ -140,8 +140,8 @@ class MultiStagePattern(WorkflowPattern):
140
140
  stage_routing = []
141
141
  for i, stage in enumerate(stages):
142
142
  stage_routing.append(
143
- f''' if stage_name == "{stage}":
144
- return await self._{stage}(input_data, tier)'''
143
+ f""" if stage_name == "{stage}":
144
+ return await self._{stage}(input_data, tier)"""
145
145
  )
146
146
 
147
147
  stage_routing_code = "\n".join(stage_routing)
@@ -181,10 +181,10 @@ class MultiStagePattern(WorkflowPattern):
181
181
  return [
182
182
  CodeSection(
183
183
  location="class_attributes",
184
- code=f''' name = "{workflow_name}"
184
+ code=f""" name = "{workflow_name}"
185
185
  description = "{description}"
186
186
  stages = {stages}
187
- {tier_map_code}''',
187
+ {tier_map_code}""",
188
188
  priority=1,
189
189
  ),
190
190
  CodeSection(
@@ -236,13 +236,13 @@ class CrewBasedPattern(WorkflowPattern):
236
236
  return [
237
237
  CodeSection(
238
238
  location="class_attributes",
239
- code=f''' name = "{workflow_name}"
239
+ code=f""" name = "{workflow_name}"
240
240
  description = "{description}"
241
241
  stages = ["analyze", "fix"]
242
242
  tier_map = {{
243
243
  "analyze": ModelTier.CAPABLE,
244
244
  "fix": ModelTier.CAPABLE,
245
- }}''',
245
+ }}""",
246
246
  priority=1,
247
247
  ),
248
248
  CodeSection(