empathy-framework 5.0.1__py3-none-any.whl → 5.0.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 (38) hide show
  1. {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/METADATA +53 -9
  2. {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/RECORD +28 -31
  3. empathy_llm_toolkit/providers.py +175 -35
  4. empathy_llm_toolkit/utils/tokens.py +150 -30
  5. empathy_os/__init__.py +1 -1
  6. empathy_os/cli/commands/batch.py +256 -0
  7. empathy_os/cli/commands/cache.py +248 -0
  8. empathy_os/cli/commands/inspect.py +1 -2
  9. empathy_os/cli/commands/metrics.py +1 -1
  10. empathy_os/cli/commands/routing.py +285 -0
  11. empathy_os/cli/commands/workflow.py +2 -2
  12. empathy_os/cli/parsers/__init__.py +6 -0
  13. empathy_os/cli/parsers/batch.py +118 -0
  14. empathy_os/cli/parsers/cache.py +65 -0
  15. empathy_os/cli/parsers/routing.py +110 -0
  16. empathy_os/dashboard/standalone_server.py +22 -11
  17. empathy_os/metrics/collector.py +31 -0
  18. empathy_os/models/token_estimator.py +21 -13
  19. empathy_os/telemetry/agent_coordination.py +12 -14
  20. empathy_os/telemetry/agent_tracking.py +18 -19
  21. empathy_os/telemetry/approval_gates.py +27 -39
  22. empathy_os/telemetry/event_streaming.py +19 -19
  23. empathy_os/telemetry/feedback_loop.py +13 -16
  24. empathy_os/workflows/batch_processing.py +56 -10
  25. empathy_os/vscode_bridge 2.py +0 -173
  26. empathy_os/workflows/progressive/README 2.md +0 -454
  27. empathy_os/workflows/progressive/__init__ 2.py +0 -92
  28. empathy_os/workflows/progressive/cli 2.py +0 -242
  29. empathy_os/workflows/progressive/core 2.py +0 -488
  30. empathy_os/workflows/progressive/orchestrator 2.py +0 -701
  31. empathy_os/workflows/progressive/reports 2.py +0 -528
  32. empathy_os/workflows/progressive/telemetry 2.py +0 -280
  33. empathy_os/workflows/progressive/test_gen 2.py +0 -514
  34. empathy_os/workflows/progressive/workflow 2.py +0 -628
  35. {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/WHEEL +0 -0
  36. {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/entry_points.txt +0 -0
  37. {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/licenses/LICENSE +0 -0
  38. {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/top_level.txt +0 -0
@@ -7,10 +7,35 @@ Copyright 2025 Smart-AI-Memory
7
7
  Licensed under Fair Source License 0.9
8
8
  """
9
9
 
10
+ import functools
11
+ import logging
12
+ import os
13
+ from dataclasses import dataclass
10
14
  from typing import Any
11
15
 
12
- # Lazy import to avoid requiring anthropic if not used
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Lazy import to avoid requiring dependencies if not used
13
19
  _client = None
20
+ _tiktoken_encoding = None
21
+
22
+ # Try to import tiktoken for fast local estimation
23
+ try:
24
+ import tiktoken
25
+
26
+ TIKTOKEN_AVAILABLE = True
27
+ except ImportError:
28
+ TIKTOKEN_AVAILABLE = False
29
+ logger.debug("tiktoken not available - will use API or heuristic fallback")
30
+
31
+
32
+ @dataclass
33
+ class TokenCount:
34
+ """Token count result with metadata."""
35
+
36
+ tokens: int
37
+ method: str # "anthropic_api", "tiktoken", "heuristic"
38
+ model: str | None = None
14
39
 
15
40
 
16
41
  def _get_client():
@@ -20,7 +45,12 @@ def _get_client():
20
45
  try:
21
46
  from anthropic import Anthropic
22
47
 
23
- _client = Anthropic()
48
+ api_key = os.getenv("ANTHROPIC_API_KEY")
49
+ if not api_key:
50
+ raise ValueError(
51
+ "ANTHROPIC_API_KEY environment variable required for API token counting"
52
+ )
53
+ _client = Anthropic(api_key=api_key)
24
54
  except ImportError as e:
25
55
  raise ImportError(
26
56
  "anthropic package required for token counting. Install with: pip install anthropic"
@@ -28,57 +58,109 @@ def _get_client():
28
58
  return _client
29
59
 
30
60
 
31
- def count_tokens(text: str, model: str = "claude-sonnet-4-5") -> int:
32
- """Count tokens using Anthropic's tokenizer.
61
+ @functools.lru_cache(maxsize=4)
62
+ def _get_tiktoken_encoding(model: str) -> Any:
63
+ """Get tiktoken encoding for Claude models (cached)."""
64
+ if not TIKTOKEN_AVAILABLE:
65
+ return None
66
+ try:
67
+ # Claude uses cl100k_base encoding (similar to GPT-4)
68
+ return tiktoken.get_encoding("cl100k_base")
69
+ except Exception as e:
70
+ logger.warning(f"Failed to get tiktoken encoding: {e}")
71
+ return None
72
+
73
+
74
+ def _count_tokens_tiktoken(text: str, model: str) -> int:
75
+ """Count tokens using tiktoken (fast local estimation)."""
76
+ if not text:
77
+ return 0
78
+
79
+ encoding = _get_tiktoken_encoding(model)
80
+ if not encoding:
81
+ return 0
82
+
83
+ try:
84
+ return len(encoding.encode(text))
85
+ except Exception as e:
86
+ logger.warning(f"tiktoken encoding failed: {e}")
87
+ return 0
88
+
89
+
90
+ def _count_tokens_heuristic(text: str) -> int:
91
+ """Fallback heuristic token counting (~4 chars per token)."""
92
+ if not text:
93
+ return 0
94
+ return max(1, len(text) // 4)
95
+
96
+
97
+ def count_tokens(text: str, model: str = "claude-sonnet-4-5-20250929", use_api: bool = False) -> int:
98
+ """Count tokens using best available method.
99
+
100
+ By default, uses tiktoken for fast local estimation (~98% accurate).
101
+ Set use_api=True for exact count via Anthropic API (requires network call).
33
102
 
34
103
  Args:
35
104
  text: Text to tokenize
36
105
  model: Model ID (different models may have different tokenizers)
106
+ use_api: Whether to use Anthropic API for exact count (slower, requires API key)
37
107
 
38
108
  Returns:
39
- Exact token count as would be billed by API
109
+ Token count
40
110
 
41
111
  Example:
42
112
  >>> count_tokens("Hello, world!")
43
113
  4
44
- >>> count_tokens("def hello():\\n print('hi')")
114
+ >>> count_tokens("def hello():\\n print('hi')", use_api=True)
45
115
  8
46
116
 
47
117
  Raises:
48
- ImportError: If anthropic package not installed
49
- ValueError: If text is invalid
118
+ ImportError: If anthropic package not installed (when use_api=True)
119
+ ValueError: If API key missing (when use_api=True)
50
120
 
51
121
  """
52
122
  if not text:
53
123
  return 0
54
124
 
55
- client = _get_client()
56
-
57
- # Use Anthropic's count_tokens method
58
- # Note: This is a synchronous call for simplicity
59
- try:
60
- result = client.count_tokens(text)
61
- return result
62
- except Exception as e:
63
- # Fallback to rough estimate if API fails
64
- # This ensures token counting never crashes workflows
65
- import logging
66
-
67
- logging.debug(f"Token counting failed, using fallback estimate: {e}")
68
- return len(text) // 4
125
+ # Use API if explicitly requested
126
+ if use_api:
127
+ try:
128
+ client = _get_client()
129
+ # FIXED: Use correct API method - client.messages.count_tokens()
130
+ result = client.messages.count_tokens(
131
+ model=model,
132
+ messages=[{"role": "user", "content": text}],
133
+ )
134
+ return int(result.input_tokens)
135
+ except Exception as e:
136
+ logger.warning(f"API token counting failed, using fallback: {e}")
137
+ # Continue to fallback methods
138
+
139
+ # Try tiktoken first (fast and accurate)
140
+ if TIKTOKEN_AVAILABLE:
141
+ tokens = _count_tokens_tiktoken(text, model)
142
+ if tokens > 0:
143
+ return tokens
144
+
145
+ # Fallback to heuristic
146
+ return _count_tokens_heuristic(text)
69
147
 
70
148
 
71
149
  def count_message_tokens(
72
150
  messages: list[dict[str, str]],
73
151
  system_prompt: str | None = None,
74
- model: str = "claude-sonnet-4-5",
152
+ model: str = "claude-sonnet-4-5-20250929",
153
+ use_api: bool = False,
75
154
  ) -> dict[str, int]:
76
155
  """Count tokens in a conversation.
77
156
 
157
+ By default uses tiktoken for fast estimation. Set use_api=True for exact count.
158
+
78
159
  Args:
79
160
  messages: List of message dicts with "role" and "content"
80
161
  system_prompt: Optional system prompt
81
162
  model: Model ID
163
+ use_api: Whether to use Anthropic API for exact count
82
164
 
83
165
  Returns:
84
166
  Dict with token counts by component:
@@ -92,21 +174,59 @@ def count_message_tokens(
92
174
  {"system": 4, "messages": 6, "total": 10}
93
175
 
94
176
  """
177
+ if not messages:
178
+ if system_prompt:
179
+ tokens = count_tokens(system_prompt, model, use_api)
180
+ return {"system": tokens, "messages": 0, "total": tokens}
181
+ return {"system": 0, "messages": 0, "total": 0}
182
+
183
+ # Use Anthropic API for exact count if requested
184
+ if use_api:
185
+ try:
186
+ client = _get_client()
187
+ kwargs: dict[str, Any] = {"model": model, "messages": messages}
188
+ if system_prompt:
189
+ kwargs["system"] = system_prompt
190
+
191
+ result = client.messages.count_tokens(**kwargs)
192
+ # API returns total input tokens, estimate breakdown
193
+ total_tokens = result.input_tokens
194
+
195
+ # Estimate system vs message breakdown
196
+ if system_prompt:
197
+ system_tokens = count_tokens(system_prompt, model, use_api=False)
198
+ message_tokens = max(0, total_tokens - system_tokens)
199
+ else:
200
+ system_tokens = 0
201
+ message_tokens = total_tokens
202
+
203
+ return {
204
+ "system": system_tokens,
205
+ "messages": message_tokens,
206
+ "total": total_tokens,
207
+ }
208
+ except Exception as e:
209
+ logger.warning(f"API token counting failed, using fallback: {e}")
210
+ # Continue to fallback method
211
+
212
+ # Fallback: count each component separately
95
213
  counts: dict[str, int] = {}
96
214
 
97
215
  # Count system prompt
98
216
  if system_prompt:
99
- counts["system"] = count_tokens(system_prompt, model)
217
+ counts["system"] = count_tokens(system_prompt, model, use_api=False)
100
218
  else:
101
219
  counts["system"] = 0
102
220
 
103
- # Count messages
104
- # Combine all messages into single text for accurate tokenization
105
- message_text = "\n".join(f"{msg['role']}: {msg['content']}" for msg in messages)
106
- counts["messages"] = count_tokens(message_text, model)
221
+ # Count messages with overhead
222
+ message_tokens = 0
223
+ for message in messages:
224
+ content = message.get("content", "")
225
+ message_tokens += count_tokens(content, model, use_api=False)
226
+ message_tokens += 4 # Overhead for role markers
107
227
 
108
- # Total
109
- counts["total"] = counts["system"] + counts["messages"]
228
+ counts["messages"] = message_tokens
229
+ counts["total"] = counts["system"] + message_tokens
110
230
 
111
231
  return counts
112
232
 
empathy_os/__init__.py CHANGED
@@ -55,7 +55,7 @@ Copyright 2025 Smart AI Memory, LLC
55
55
  Licensed under Fair Source 0.9
56
56
  """
57
57
 
58
- __version__ = "5.0.1"
58
+ __version__ = "5.0.3"
59
59
  __author__ = "Patrick Roebuck"
60
60
  __email__ = "patrick.roebuck@smartaimemory.com"
61
61
 
@@ -0,0 +1,256 @@
1
+ """CLI commands for Anthropic Batch API operations (50% cost savings).
2
+
3
+ Provides commands to submit, monitor, and retrieve results from batch processing jobs.
4
+
5
+ Copyright 2025 Smart-AI-Memory
6
+ Licensed under Fair Source License 0.9
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ import os
13
+ from pathlib import Path
14
+
15
+ from empathy_os.config import _validate_file_path
16
+ from empathy_os.workflows.batch_processing import BatchProcessingWorkflow
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def cmd_batch_submit(args):
22
+ """Submit a batch processing job from JSON file.
23
+
24
+ Args:
25
+ args: Arguments with input_file path
26
+
27
+ File format:
28
+ [
29
+ {
30
+ "task_id": "task_1",
31
+ "task_type": "analyze_logs",
32
+ "input_data": {"logs": "ERROR: ..."},
33
+ "model_tier": "capable"
34
+ },
35
+ ...
36
+ ]
37
+ """
38
+ input_file = Path(args.input_file)
39
+ if not input_file.exists():
40
+ print(f"❌ Error: Input file not found: {input_file}")
41
+ return 1
42
+
43
+ print(f"📤 Submitting batch from {input_file}...")
44
+
45
+ try:
46
+ # Get API key from environment
47
+ api_key = os.getenv("ANTHROPIC_API_KEY")
48
+ if not api_key:
49
+ print("❌ Error: ANTHROPIC_API_KEY environment variable not set")
50
+ return 1
51
+
52
+ # Load requests from file
53
+ workflow = BatchProcessingWorkflow(api_key=api_key)
54
+ requests = workflow.load_requests_from_file(str(input_file))
55
+
56
+ print(f" Found {len(requests)} requests")
57
+
58
+ # Create batch (sync operation)
59
+ batch_id = workflow.batch_provider.create_batch(
60
+ [
61
+ {
62
+ "custom_id": req.task_id,
63
+ "params": {
64
+ "model": "claude-sonnet-4-5-20250929", # Default model
65
+ "messages": workflow._format_messages(req),
66
+ "max_tokens": 4096,
67
+ },
68
+ }
69
+ for req in requests
70
+ ]
71
+ )
72
+
73
+ print(f"\n✅ Batch submitted successfully!")
74
+ print(f" Batch ID: {batch_id}")
75
+ print(f"\nMonitor status with: empathy batch status {batch_id}")
76
+ print(f"Retrieve results with: empathy batch results {batch_id} output.json")
77
+ print(
78
+ f"Or wait for completion: empathy batch wait {batch_id} output.json --poll-interval 300"
79
+ )
80
+
81
+ return 0
82
+
83
+ except Exception as e:
84
+ logger.exception("Failed to submit batch")
85
+ print(f"❌ Error: {e}")
86
+ return 1
87
+
88
+
89
+ def cmd_batch_status(args):
90
+ """Check status of a batch processing job.
91
+
92
+ Args:
93
+ args: Arguments with batch_id
94
+ """
95
+ batch_id = args.batch_id
96
+
97
+ print(f"🔍 Checking status for batch {batch_id}...")
98
+
99
+ try:
100
+ api_key = os.getenv("ANTHROPIC_API_KEY")
101
+ if not api_key:
102
+ print("❌ Error: ANTHROPIC_API_KEY environment variable not set")
103
+ return 1
104
+
105
+ workflow = BatchProcessingWorkflow(api_key=api_key)
106
+ status = workflow.batch_provider.get_batch_status(batch_id)
107
+
108
+ print(f"\n📊 Batch Status:")
109
+ print(f" ID: {status.id}")
110
+ print(f" Processing Status: {status.processing_status}")
111
+ print(f" Created: {status.created_at}")
112
+
113
+ if hasattr(status, "ended_at") and status.ended_at:
114
+ print(f" Ended: {status.ended_at}")
115
+
116
+ print(f"\n📈 Request Counts:")
117
+ counts = status.request_counts
118
+ print(f" Processing: {counts.processing}")
119
+ print(f" Succeeded: {counts.succeeded}")
120
+ print(f" Errored: {counts.errored}")
121
+ print(f" Canceled: {counts.canceled}")
122
+ print(f" Expired: {counts.expired}")
123
+
124
+ if status.processing_status == "ended":
125
+ print(f"\n✅ Batch processing completed!")
126
+ print(f" Retrieve results with: empathy batch results {batch_id} output.json")
127
+ else:
128
+ print(f"\n⏳ Batch still processing...")
129
+
130
+ # Output JSON if requested
131
+ if args.json:
132
+ print("\n" + json.dumps(status.__dict__, indent=2, default=str))
133
+
134
+ return 0
135
+
136
+ except Exception as e:
137
+ logger.exception("Failed to get batch status")
138
+ print(f"❌ Error: {e}")
139
+ return 1
140
+
141
+
142
+ def cmd_batch_results(args):
143
+ """Retrieve results from a completed batch.
144
+
145
+ Args:
146
+ args: Arguments with batch_id and output_file
147
+ """
148
+ batch_id = args.batch_id
149
+ output_file = args.output_file
150
+
151
+ print(f"📥 Retrieving results for batch {batch_id}...")
152
+
153
+ try:
154
+ api_key = os.getenv("ANTHROPIC_API_KEY")
155
+ if not api_key:
156
+ print("❌ Error: ANTHROPIC_API_KEY environment variable not set")
157
+ return 1
158
+
159
+ workflow = BatchProcessingWorkflow(api_key=api_key)
160
+
161
+ # Check status first
162
+ status = workflow.batch_provider.get_batch_status(batch_id)
163
+
164
+ if status.processing_status != "ended":
165
+ print(f"❌ Error: Batch has not ended processing (status: {status.processing_status})")
166
+ print(f" Wait for completion with: empathy batch wait {batch_id} {output_file}")
167
+ return 1
168
+
169
+ # Get results
170
+ results = workflow.batch_provider.get_batch_results(batch_id)
171
+
172
+ # Save to file
173
+ validated_path = _validate_file_path(output_file)
174
+ with open(validated_path, "w") as f:
175
+ json.dump([dict(r) for r in results], f, indent=2, default=str)
176
+
177
+ print(f"\n✅ Results saved to {validated_path}")
178
+ print(f" Total: {len(results)} results")
179
+
180
+ # Summary
181
+ succeeded = sum(
182
+ 1 for r in results if r.get("result", {}).get("type") == "succeeded"
183
+ )
184
+ errored = sum(
185
+ 1 for r in results if r.get("result", {}).get("type") == "errored"
186
+ )
187
+
188
+ print(f" Succeeded: {succeeded}")
189
+ print(f" Errored: {errored}")
190
+
191
+ return 0
192
+
193
+ except Exception as e:
194
+ logger.exception("Failed to retrieve results")
195
+ print(f"❌ Error: {e}")
196
+ return 1
197
+
198
+
199
+ def cmd_batch_wait(args):
200
+ """Wait for batch to complete and retrieve results.
201
+
202
+ Args:
203
+ args: Arguments with batch_id, output_file, poll_interval, timeout
204
+ """
205
+ batch_id = args.batch_id
206
+ output_file = args.output_file
207
+ poll_interval = args.poll_interval
208
+ timeout = args.timeout
209
+
210
+ print(f"⏳ Waiting for batch {batch_id} to complete...")
211
+ print(f" Polling every {poll_interval}s (max {timeout}s)")
212
+
213
+ try:
214
+ api_key = os.getenv("ANTHROPIC_API_KEY")
215
+ if not api_key:
216
+ print("❌ Error: ANTHROPIC_API_KEY environment variable not set")
217
+ return 1
218
+
219
+ workflow = BatchProcessingWorkflow(api_key=api_key)
220
+
221
+ # Wait for completion (async)
222
+ results = asyncio.run(
223
+ workflow.batch_provider.wait_for_batch(
224
+ batch_id, poll_interval=poll_interval, timeout=timeout
225
+ )
226
+ )
227
+
228
+ # Save results
229
+ validated_path = _validate_file_path(output_file)
230
+ with open(validated_path, "w") as f:
231
+ json.dump([dict(r) for r in results], f, indent=2, default=str)
232
+
233
+ print(f"\n✅ Batch completed! Results saved to {validated_path}")
234
+ print(f" Total: {len(results)} results")
235
+
236
+ # Summary
237
+ succeeded = sum(
238
+ 1 for r in results if r.get("result", {}).get("type") == "succeeded"
239
+ )
240
+ errored = sum(
241
+ 1 for r in results if r.get("result", {}).get("type") == "errored"
242
+ )
243
+
244
+ print(f" Succeeded: {succeeded}")
245
+ print(f" Errored: {errored}")
246
+
247
+ return 0
248
+
249
+ except TimeoutError:
250
+ print(f"\n⏰ Timeout: Batch did not complete within {timeout}s")
251
+ print(f" Check status with: empathy batch status {batch_id}")
252
+ return 1
253
+ except Exception as e:
254
+ logger.exception("Failed to wait for batch")
255
+ print(f"❌ Error: {e}")
256
+ return 1