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.
- {empathy_framework-3.7.1.dist-info → empathy_framework-3.8.0.dist-info}/METADATA +130 -11
- {empathy_framework-3.7.1.dist-info → empathy_framework-3.8.0.dist-info}/RECORD +21 -15
- empathy_os/cache/__init__.py +117 -0
- empathy_os/cache/base.py +166 -0
- empathy_os/cache/dependency_manager.py +253 -0
- empathy_os/cache/hash_only.py +248 -0
- empathy_os/cache/hybrid.py +390 -0
- empathy_os/cache/storage.py +282 -0
- empathy_os/config.py +2 -1
- empathy_os/memory/long_term.py +8 -7
- empathy_os/workflows/base.py +131 -1
- empathy_os/workflows/new_sample_workflow1_README.md +1 -1
- empathy_os/workflows/refactor_plan.py +4 -2
- empathy_os/workflows/security_audit.py +2 -6
- empathy_os/workflows/test5_README.md +1 -1
- hot_reload/__init__.py +3 -3
- workflow_patterns/structural.py +8 -8
- {empathy_framework-3.7.1.dist-info → empathy_framework-3.8.0.dist-info}/WHEEL +0 -0
- {empathy_framework-3.7.1.dist-info → empathy_framework-3.8.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-3.7.1.dist-info → empathy_framework-3.8.0.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-3.7.1.dist-info → empathy_framework-3.8.0.dist-info}/top_level.txt +0 -0
empathy_os/memory/long_term.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
empathy_os/workflows/base.py
CHANGED
|
@@ -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=
|
|
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
|
|
|
@@ -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
|
|
486
|
-
#
|
|
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(
|
|
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()},
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
ReloadNotificationManager,
|
|
42
|
+
create_notification_callback,
|
|
43
|
+
get_notification_manager,
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
__all__ = [
|
workflow_patterns/structural.py
CHANGED
|
@@ -41,12 +41,12 @@ class SingleStagePattern(WorkflowPattern):
|
|
|
41
41
|
return [
|
|
42
42
|
CodeSection(
|
|
43
43
|
location="class_attributes",
|
|
44
|
-
code=f
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|