empathy-framework 4.6.2__py3-none-any.whl → 4.6.3__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-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/METADATA +1 -1
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/RECORD +53 -20
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/WHEEL +1 -1
- empathy_os/__init__.py +1 -1
- empathy_os/cli.py +361 -32
- empathy_os/config/xml_config.py +8 -3
- empathy_os/core.py +37 -4
- empathy_os/leverage_points.py +2 -1
- empathy_os/memory/short_term.py +45 -1
- empathy_os/meta_workflows/agent_creator 2.py +254 -0
- empathy_os/meta_workflows/builtin_templates 2.py +567 -0
- empathy_os/meta_workflows/cli_meta_workflows 2.py +1551 -0
- empathy_os/meta_workflows/form_engine 2.py +304 -0
- empathy_os/meta_workflows/intent_detector 2.py +298 -0
- empathy_os/meta_workflows/pattern_learner 2.py +754 -0
- empathy_os/meta_workflows/session_context 2.py +398 -0
- empathy_os/meta_workflows/template_registry 2.py +229 -0
- empathy_os/meta_workflows/workflow 2.py +980 -0
- empathy_os/models/token_estimator.py +16 -9
- empathy_os/models/validation.py +7 -1
- empathy_os/orchestration/pattern_learner 2.py +699 -0
- empathy_os/orchestration/real_tools 2.py +938 -0
- empathy_os/orchestration/real_tools.py +4 -2
- empathy_os/socratic/__init__ 2.py +273 -0
- empathy_os/socratic/ab_testing 2.py +969 -0
- empathy_os/socratic/blueprint 2.py +532 -0
- empathy_os/socratic/cli 2.py +689 -0
- empathy_os/socratic/collaboration 2.py +1112 -0
- empathy_os/socratic/domain_templates 2.py +916 -0
- empathy_os/socratic/embeddings 2.py +734 -0
- empathy_os/socratic/engine 2.py +729 -0
- empathy_os/socratic/explainer 2.py +663 -0
- empathy_os/socratic/feedback 2.py +767 -0
- empathy_os/socratic/forms 2.py +624 -0
- empathy_os/socratic/generator 2.py +716 -0
- empathy_os/socratic/llm_analyzer 2.py +635 -0
- empathy_os/socratic/mcp_server 2.py +751 -0
- empathy_os/socratic/session 2.py +306 -0
- empathy_os/socratic/storage 2.py +635 -0
- empathy_os/socratic/storage.py +2 -1
- empathy_os/socratic/success 2.py +719 -0
- empathy_os/socratic/visual_editor 2.py +812 -0
- empathy_os/socratic/web_ui 2.py +925 -0
- empathy_os/tier_recommender.py +5 -2
- empathy_os/workflow_commands.py +11 -6
- empathy_os/workflows/base.py +1 -1
- empathy_os/workflows/batch_processing 2.py +310 -0
- empathy_os/workflows/release_prep_crew 2.py +968 -0
- empathy_os/workflows/test_coverage_boost_crew 2.py +848 -0
- empathy_os/workflows/test_maintenance.py +3 -2
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/entry_points.txt +0 -0
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/top_level.txt +0 -0
empathy_os/tier_recommender.py
CHANGED
|
@@ -23,6 +23,8 @@ from collections import defaultdict
|
|
|
23
23
|
from dataclasses import dataclass
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
|
|
26
|
+
from empathy_os.config import _validate_file_path
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
@dataclass
|
|
28
30
|
class TierRecommendationResult:
|
|
@@ -86,7 +88,8 @@ class TierRecommender:
|
|
|
86
88
|
|
|
87
89
|
for file_path in self.patterns_dir.glob("*.json"):
|
|
88
90
|
try:
|
|
89
|
-
|
|
91
|
+
validated_path = _validate_file_path(str(file_path))
|
|
92
|
+
with open(validated_path) as f:
|
|
90
93
|
data = json.load(f)
|
|
91
94
|
|
|
92
95
|
# Check if this is an enhanced pattern
|
|
@@ -97,7 +100,7 @@ class TierRecommender:
|
|
|
97
100
|
for pattern in data["patterns"]:
|
|
98
101
|
if "tier_progression" in pattern:
|
|
99
102
|
patterns.append(pattern)
|
|
100
|
-
except (json.JSONDecodeError, KeyError):
|
|
103
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
|
101
104
|
continue
|
|
102
105
|
|
|
103
106
|
return patterns
|
empathy_os/workflow_commands.py
CHANGED
|
@@ -16,6 +16,7 @@ from datetime import datetime, timedelta
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any
|
|
18
18
|
|
|
19
|
+
from empathy_os.config import _validate_file_path
|
|
19
20
|
from empathy_os.logging_config import get_logger
|
|
20
21
|
|
|
21
22
|
logger = get_logger(__name__)
|
|
@@ -33,10 +34,11 @@ def _load_patterns(patterns_dir: str = "./patterns") -> dict[str, list]:
|
|
|
33
34
|
file_path = patterns_path / f"{pattern_type}.json"
|
|
34
35
|
if file_path.exists():
|
|
35
36
|
try:
|
|
36
|
-
|
|
37
|
+
validated_path = _validate_file_path(str(file_path))
|
|
38
|
+
with open(validated_path) as f:
|
|
37
39
|
data = json.load(f)
|
|
38
40
|
patterns[pattern_type] = data.get("patterns", data.get("items", []))
|
|
39
|
-
except (OSError, json.JSONDecodeError):
|
|
41
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
40
42
|
pass
|
|
41
43
|
|
|
42
44
|
return patterns
|
|
@@ -47,10 +49,11 @@ def _load_stats(empathy_dir: str = ".empathy") -> dict[str, Any]:
|
|
|
47
49
|
stats_file = Path(empathy_dir) / "stats.json"
|
|
48
50
|
if stats_file.exists():
|
|
49
51
|
try:
|
|
50
|
-
|
|
52
|
+
validated_path = _validate_file_path(str(stats_file))
|
|
53
|
+
with open(validated_path) as f:
|
|
51
54
|
result: dict[str, Any] = json.load(f)
|
|
52
55
|
return result
|
|
53
|
-
except (OSError, json.JSONDecodeError):
|
|
56
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
54
57
|
pass
|
|
55
58
|
return {"commands": {}, "last_session": None, "patterns_learned": 0}
|
|
56
59
|
|
|
@@ -60,7 +63,8 @@ def _save_stats(stats: dict, empathy_dir: str = ".empathy") -> None:
|
|
|
60
63
|
stats_dir = Path(empathy_dir)
|
|
61
64
|
stats_dir.mkdir(parents=True, exist_ok=True)
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
validated_path = _validate_file_path(str(stats_dir / "stats.json"))
|
|
67
|
+
with open(validated_path, "w") as f:
|
|
64
68
|
json.dump(stats, f, indent=2, default=str)
|
|
65
69
|
|
|
66
70
|
|
|
@@ -84,7 +88,8 @@ def _get_tech_debt_trend(patterns_dir: str = "./patterns") -> str:
|
|
|
84
88
|
return "unknown"
|
|
85
89
|
|
|
86
90
|
try:
|
|
87
|
-
|
|
91
|
+
validated_path = _validate_file_path(str(tech_debt_file))
|
|
92
|
+
with open(validated_path) as f:
|
|
88
93
|
data = json.load(f)
|
|
89
94
|
|
|
90
95
|
snapshots = data.get("snapshots", [])
|
empathy_os/workflows/base.py
CHANGED
|
@@ -277,7 +277,7 @@ def _save_workflow_run(
|
|
|
277
277
|
history.append(run)
|
|
278
278
|
history = history[-max_history:]
|
|
279
279
|
|
|
280
|
-
validated_path = _validate_file_path(path)
|
|
280
|
+
validated_path = _validate_file_path(str(path))
|
|
281
281
|
with open(validated_path, "w") as f:
|
|
282
282
|
json.dump(history, f, indent=2)
|
|
283
283
|
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Batch Processing Workflow using Anthropic Batch API.
|
|
2
|
+
|
|
3
|
+
Enables 50% cost reduction by processing non-urgent tasks asynchronously.
|
|
4
|
+
Batch API processes requests within 24 hours - not suitable for interactive workflows.
|
|
5
|
+
|
|
6
|
+
Copyright 2025 Smart-AI-Memory
|
|
7
|
+
Licensed under Fair Source License 0.9
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from empathy_llm_toolkit.providers import AnthropicBatchProvider
|
|
17
|
+
from empathy_os.models import get_model
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class BatchRequest:
|
|
24
|
+
"""Single request in a batch."""
|
|
25
|
+
|
|
26
|
+
task_id: str
|
|
27
|
+
task_type: str
|
|
28
|
+
input_data: dict[str, Any]
|
|
29
|
+
model_tier: str = "capable" # cheap, capable, premium
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class BatchResult:
|
|
34
|
+
"""Result from batch processing."""
|
|
35
|
+
|
|
36
|
+
task_id: str
|
|
37
|
+
success: bool
|
|
38
|
+
output: dict[str, Any] | None = None
|
|
39
|
+
error: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BatchProcessingWorkflow:
|
|
43
|
+
"""Process multiple tasks via Anthropic Batch API (50% cost savings).
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> workflow = BatchProcessingWorkflow()
|
|
47
|
+
>>> requests = [
|
|
48
|
+
... BatchRequest(
|
|
49
|
+
... task_id="task_1",
|
|
50
|
+
... task_type="analyze_logs",
|
|
51
|
+
... input_data={"logs": "ERROR: Connection failed..."}
|
|
52
|
+
... ),
|
|
53
|
+
... BatchRequest(
|
|
54
|
+
... task_id="task_2",
|
|
55
|
+
... task_type="generate_report",
|
|
56
|
+
... input_data={"data": {...}}
|
|
57
|
+
... )
|
|
58
|
+
... ]
|
|
59
|
+
>>> results = await workflow.execute_batch(requests)
|
|
60
|
+
>>> print(f"{sum(r.success for r in results)}/{len(results)} successful")
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, api_key: str | None = None):
|
|
64
|
+
"""Initialize batch workflow.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
api_key: Anthropic API key (optional, uses ANTHROPIC_API_KEY env var)
|
|
68
|
+
"""
|
|
69
|
+
self.batch_provider = AnthropicBatchProvider(api_key=api_key)
|
|
70
|
+
|
|
71
|
+
async def execute_batch(
|
|
72
|
+
self,
|
|
73
|
+
requests: list[BatchRequest],
|
|
74
|
+
poll_interval: int = 300, # 5 minutes
|
|
75
|
+
timeout: int = 86400, # 24 hours
|
|
76
|
+
) -> list[BatchResult]:
|
|
77
|
+
"""Execute batch of requests.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
requests: List of batch requests
|
|
81
|
+
poll_interval: Seconds between status checks (default: 300)
|
|
82
|
+
timeout: Maximum wait time in seconds (default: 86400)
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of results matching input order
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ValueError: If requests is empty
|
|
89
|
+
TimeoutError: If batch doesn't complete within timeout
|
|
90
|
+
RuntimeError: If batch processing fails
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
>>> workflow = BatchProcessingWorkflow()
|
|
94
|
+
>>> requests = [
|
|
95
|
+
... BatchRequest(
|
|
96
|
+
... task_id="task_1",
|
|
97
|
+
... task_type="analyze_logs",
|
|
98
|
+
... input_data={"logs": "..."}
|
|
99
|
+
... )
|
|
100
|
+
... ]
|
|
101
|
+
>>> results = await workflow.execute_batch(requests)
|
|
102
|
+
>>> for result in results:
|
|
103
|
+
... if result.success:
|
|
104
|
+
... print(f"Task {result.task_id}: Success")
|
|
105
|
+
... else:
|
|
106
|
+
... print(f"Task {result.task_id}: {result.error}")
|
|
107
|
+
"""
|
|
108
|
+
if not requests:
|
|
109
|
+
raise ValueError("requests cannot be empty")
|
|
110
|
+
|
|
111
|
+
# Convert to Anthropic batch format
|
|
112
|
+
api_requests = []
|
|
113
|
+
for req in requests:
|
|
114
|
+
model = get_model("anthropic", req.model_tier)
|
|
115
|
+
if model is None:
|
|
116
|
+
raise ValueError(f"Unknown model tier: {req.model_tier}")
|
|
117
|
+
|
|
118
|
+
api_requests.append(
|
|
119
|
+
{
|
|
120
|
+
"custom_id": req.task_id,
|
|
121
|
+
"model": model.id,
|
|
122
|
+
"messages": self._format_messages(req),
|
|
123
|
+
"max_tokens": 4096,
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Submit batch
|
|
128
|
+
logger.info(f"Submitting batch of {len(requests)} requests")
|
|
129
|
+
batch_id = self.batch_provider.create_batch(api_requests)
|
|
130
|
+
|
|
131
|
+
logger.info(
|
|
132
|
+
f"Batch {batch_id} created, polling every {poll_interval}s "
|
|
133
|
+
f"(max {timeout}s)"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Wait for completion
|
|
137
|
+
try:
|
|
138
|
+
raw_results = await self.batch_provider.wait_for_batch(
|
|
139
|
+
batch_id, poll_interval=poll_interval, timeout=timeout
|
|
140
|
+
)
|
|
141
|
+
except TimeoutError:
|
|
142
|
+
logger.error(f"Batch {batch_id} timed out after {timeout}s")
|
|
143
|
+
return [
|
|
144
|
+
BatchResult(
|
|
145
|
+
task_id=req.task_id,
|
|
146
|
+
success=False,
|
|
147
|
+
error="Batch processing timed out",
|
|
148
|
+
)
|
|
149
|
+
for req in requests
|
|
150
|
+
]
|
|
151
|
+
except RuntimeError as e:
|
|
152
|
+
logger.error(f"Batch {batch_id} failed: {e}")
|
|
153
|
+
return [
|
|
154
|
+
BatchResult(
|
|
155
|
+
task_id=req.task_id, success=False, error=f"Batch failed: {e}"
|
|
156
|
+
)
|
|
157
|
+
for req in requests
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Parse results
|
|
161
|
+
results = []
|
|
162
|
+
for raw in raw_results:
|
|
163
|
+
task_id = raw.get("custom_id", "unknown")
|
|
164
|
+
|
|
165
|
+
if "error" in raw:
|
|
166
|
+
error_msg = raw["error"].get("message", "Unknown error")
|
|
167
|
+
results.append(
|
|
168
|
+
BatchResult(task_id=task_id, success=False, error=error_msg)
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
results.append(
|
|
172
|
+
BatchResult(
|
|
173
|
+
task_id=task_id, success=True, output=raw.get("response")
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Log summary
|
|
178
|
+
success_count = sum(r.success for r in results)
|
|
179
|
+
logger.info(
|
|
180
|
+
f"Batch {batch_id} completed: {success_count}/{len(results)} successful"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return results
|
|
184
|
+
|
|
185
|
+
def _format_messages(self, request: BatchRequest) -> list[dict[str, str]]:
|
|
186
|
+
"""Format request into Anthropic messages format.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
request: BatchRequest with task_type and input_data
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of message dicts for Anthropic API
|
|
193
|
+
"""
|
|
194
|
+
# Task-specific prompts
|
|
195
|
+
task_prompts = {
|
|
196
|
+
"analyze_logs": "Analyze the following logs and identify issues:\n\n{logs}",
|
|
197
|
+
"generate_report": "Generate a report based on:\n\n{data}",
|
|
198
|
+
"classify_bulk": "Classify the following items:\n\n{items}",
|
|
199
|
+
"generate_docs": "Generate documentation for:\n\n{code}",
|
|
200
|
+
"generate_tests": "Generate unit tests for:\n\n{code}",
|
|
201
|
+
# Add more task types as needed
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# Get prompt template or use default
|
|
205
|
+
prompt_template = task_prompts.get(
|
|
206
|
+
request.task_type, "Process the following:\n\n{input}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Format with input data
|
|
210
|
+
try:
|
|
211
|
+
content = prompt_template.format(**request.input_data)
|
|
212
|
+
except KeyError as e:
|
|
213
|
+
logger.warning(
|
|
214
|
+
f"Missing required field {e} for task {request.task_type}, "
|
|
215
|
+
f"using raw input"
|
|
216
|
+
)
|
|
217
|
+
content = prompt_template.format(input=json.dumps(request.input_data))
|
|
218
|
+
|
|
219
|
+
return [{"role": "user", "content": content}]
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def from_json_file(cls, file_path: str) -> "BatchProcessingWorkflow":
|
|
223
|
+
"""Create workflow from JSON input file.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
file_path: Path to JSON file with batch requests
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
BatchProcessingWorkflow instance
|
|
230
|
+
|
|
231
|
+
Input file format:
|
|
232
|
+
[
|
|
233
|
+
{
|
|
234
|
+
"task_id": "1",
|
|
235
|
+
"task_type": "analyze_logs",
|
|
236
|
+
"input_data": {"logs": "ERROR: ..."},
|
|
237
|
+
"model_tier": "capable"
|
|
238
|
+
},
|
|
239
|
+
...
|
|
240
|
+
]
|
|
241
|
+
"""
|
|
242
|
+
return cls()
|
|
243
|
+
|
|
244
|
+
def load_requests_from_file(self, file_path: str) -> list[BatchRequest]:
|
|
245
|
+
"""Load batch requests from JSON file.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
file_path: Path to JSON file
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of BatchRequest objects
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
FileNotFoundError: If file doesn't exist
|
|
255
|
+
json.JSONDecodeError: If file is not valid JSON
|
|
256
|
+
ValueError: If file format is invalid
|
|
257
|
+
"""
|
|
258
|
+
path = Path(file_path)
|
|
259
|
+
if not path.exists():
|
|
260
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
261
|
+
|
|
262
|
+
with open(path) as f:
|
|
263
|
+
data = json.load(f)
|
|
264
|
+
|
|
265
|
+
if not isinstance(data, list):
|
|
266
|
+
raise ValueError("Input file must contain a JSON array")
|
|
267
|
+
|
|
268
|
+
requests = []
|
|
269
|
+
for item in data:
|
|
270
|
+
if not isinstance(item, dict):
|
|
271
|
+
raise ValueError("Each item must be a JSON object")
|
|
272
|
+
|
|
273
|
+
requests.append(
|
|
274
|
+
BatchRequest(
|
|
275
|
+
task_id=item["task_id"],
|
|
276
|
+
task_type=item["task_type"],
|
|
277
|
+
input_data=item["input_data"],
|
|
278
|
+
model_tier=item.get("model_tier", "capable"),
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return requests
|
|
283
|
+
|
|
284
|
+
def save_results_to_file(
|
|
285
|
+
self, results: list[BatchResult], output_path: str
|
|
286
|
+
) -> None:
|
|
287
|
+
"""Save batch results to JSON file.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
results: List of BatchResult objects
|
|
291
|
+
output_path: Path to output file
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
OSError: If file cannot be written
|
|
295
|
+
"""
|
|
296
|
+
output_data = [
|
|
297
|
+
{
|
|
298
|
+
"task_id": r.task_id,
|
|
299
|
+
"success": r.success,
|
|
300
|
+
"output": r.output,
|
|
301
|
+
"error": r.error,
|
|
302
|
+
}
|
|
303
|
+
for r in results
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
path = Path(output_path)
|
|
307
|
+
with open(path, "w") as f:
|
|
308
|
+
json.dump(output_data, f, indent=2)
|
|
309
|
+
|
|
310
|
+
logger.info(f"Results saved to {output_path}")
|