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.
Files changed (53) hide show
  1. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/METADATA +1 -1
  2. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/RECORD +53 -20
  3. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/WHEEL +1 -1
  4. empathy_os/__init__.py +1 -1
  5. empathy_os/cli.py +361 -32
  6. empathy_os/config/xml_config.py +8 -3
  7. empathy_os/core.py +37 -4
  8. empathy_os/leverage_points.py +2 -1
  9. empathy_os/memory/short_term.py +45 -1
  10. empathy_os/meta_workflows/agent_creator 2.py +254 -0
  11. empathy_os/meta_workflows/builtin_templates 2.py +567 -0
  12. empathy_os/meta_workflows/cli_meta_workflows 2.py +1551 -0
  13. empathy_os/meta_workflows/form_engine 2.py +304 -0
  14. empathy_os/meta_workflows/intent_detector 2.py +298 -0
  15. empathy_os/meta_workflows/pattern_learner 2.py +754 -0
  16. empathy_os/meta_workflows/session_context 2.py +398 -0
  17. empathy_os/meta_workflows/template_registry 2.py +229 -0
  18. empathy_os/meta_workflows/workflow 2.py +980 -0
  19. empathy_os/models/token_estimator.py +16 -9
  20. empathy_os/models/validation.py +7 -1
  21. empathy_os/orchestration/pattern_learner 2.py +699 -0
  22. empathy_os/orchestration/real_tools 2.py +938 -0
  23. empathy_os/orchestration/real_tools.py +4 -2
  24. empathy_os/socratic/__init__ 2.py +273 -0
  25. empathy_os/socratic/ab_testing 2.py +969 -0
  26. empathy_os/socratic/blueprint 2.py +532 -0
  27. empathy_os/socratic/cli 2.py +689 -0
  28. empathy_os/socratic/collaboration 2.py +1112 -0
  29. empathy_os/socratic/domain_templates 2.py +916 -0
  30. empathy_os/socratic/embeddings 2.py +734 -0
  31. empathy_os/socratic/engine 2.py +729 -0
  32. empathy_os/socratic/explainer 2.py +663 -0
  33. empathy_os/socratic/feedback 2.py +767 -0
  34. empathy_os/socratic/forms 2.py +624 -0
  35. empathy_os/socratic/generator 2.py +716 -0
  36. empathy_os/socratic/llm_analyzer 2.py +635 -0
  37. empathy_os/socratic/mcp_server 2.py +751 -0
  38. empathy_os/socratic/session 2.py +306 -0
  39. empathy_os/socratic/storage 2.py +635 -0
  40. empathy_os/socratic/storage.py +2 -1
  41. empathy_os/socratic/success 2.py +719 -0
  42. empathy_os/socratic/visual_editor 2.py +812 -0
  43. empathy_os/socratic/web_ui 2.py +925 -0
  44. empathy_os/tier_recommender.py +5 -2
  45. empathy_os/workflow_commands.py +11 -6
  46. empathy_os/workflows/base.py +1 -1
  47. empathy_os/workflows/batch_processing 2.py +310 -0
  48. empathy_os/workflows/release_prep_crew 2.py +968 -0
  49. empathy_os/workflows/test_coverage_boost_crew 2.py +848 -0
  50. empathy_os/workflows/test_maintenance.py +3 -2
  51. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/entry_points.txt +0 -0
  52. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/licenses/LICENSE +0 -0
  53. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.3.dist-info}/top_level.txt +0 -0
@@ -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
- with open(file_path) as f:
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
@@ -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
- with open(file_path) as f:
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
- with open(stats_file) as f:
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
- with open(stats_dir / "stats.json", "w") as f:
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
- with open(tech_debt_file) as f:
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", [])
@@ -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}")