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.
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/METADATA +53 -9
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/RECORD +28 -31
- empathy_llm_toolkit/providers.py +175 -35
- empathy_llm_toolkit/utils/tokens.py +150 -30
- empathy_os/__init__.py +1 -1
- empathy_os/cli/commands/batch.py +256 -0
- empathy_os/cli/commands/cache.py +248 -0
- empathy_os/cli/commands/inspect.py +1 -2
- empathy_os/cli/commands/metrics.py +1 -1
- empathy_os/cli/commands/routing.py +285 -0
- empathy_os/cli/commands/workflow.py +2 -2
- empathy_os/cli/parsers/__init__.py +6 -0
- empathy_os/cli/parsers/batch.py +118 -0
- empathy_os/cli/parsers/cache.py +65 -0
- empathy_os/cli/parsers/routing.py +110 -0
- empathy_os/dashboard/standalone_server.py +22 -11
- empathy_os/metrics/collector.py +31 -0
- empathy_os/models/token_estimator.py +21 -13
- empathy_os/telemetry/agent_coordination.py +12 -14
- empathy_os/telemetry/agent_tracking.py +18 -19
- empathy_os/telemetry/approval_gates.py +27 -39
- empathy_os/telemetry/event_streaming.py +19 -19
- empathy_os/telemetry/feedback_loop.py +13 -16
- empathy_os/workflows/batch_processing.py +56 -10
- empathy_os/vscode_bridge 2.py +0 -173
- empathy_os/workflows/progressive/README 2.md +0 -454
- empathy_os/workflows/progressive/__init__ 2.py +0 -92
- empathy_os/workflows/progressive/cli 2.py +0 -242
- empathy_os/workflows/progressive/core 2.py +0 -488
- empathy_os/workflows/progressive/orchestrator 2.py +0 -701
- empathy_os/workflows/progressive/reports 2.py +0 -528
- empathy_os/workflows/progressive/telemetry 2.py +0 -280
- empathy_os/workflows/progressive/test_gen 2.py +0 -514
- empathy_os/workflows/progressive/workflow 2.py +0 -628
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/WHEEL +0 -0
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/entry_points.txt +0 -0
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.0.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
counts["total"] = counts["system"] +
|
|
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
|
@@ -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
|