@aj-archipelago/cortex 1.4.2 → 1.4.3
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.
- package/README.md +1 -0
- package/config.js +1 -1
- package/helper-apps/cortex-autogen2/.dockerignore +1 -0
- package/helper-apps/cortex-autogen2/Dockerfile +6 -10
- package/helper-apps/cortex-autogen2/Dockerfile.worker +2 -0
- package/helper-apps/cortex-autogen2/agents.py +203 -2
- package/helper-apps/cortex-autogen2/main.py +1 -1
- package/helper-apps/cortex-autogen2/pyproject.toml +12 -0
- package/helper-apps/cortex-autogen2/requirements.txt +14 -0
- package/helper-apps/cortex-autogen2/services/redis_publisher.py +1 -1
- package/helper-apps/cortex-autogen2/services/run_analyzer.py +1 -1
- package/helper-apps/cortex-autogen2/task_processor.py +431 -229
- package/helper-apps/cortex-autogen2/test_entity_fetcher.py +305 -0
- package/helper-apps/cortex-autogen2/tests/README.md +240 -0
- package/helper-apps/cortex-autogen2/tests/TEST_REPORT.md +342 -0
- package/helper-apps/cortex-autogen2/tests/__init__.py +8 -0
- package/helper-apps/cortex-autogen2/tests/analysis/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/analysis/improvement_suggester.py +224 -0
- package/helper-apps/cortex-autogen2/tests/analysis/trend_analyzer.py +211 -0
- package/helper-apps/cortex-autogen2/tests/cli/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/cli/run_tests.py +296 -0
- package/helper-apps/cortex-autogen2/tests/collectors/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/collectors/log_collector.py +252 -0
- package/helper-apps/cortex-autogen2/tests/collectors/progress_collector.py +182 -0
- package/helper-apps/cortex-autogen2/tests/conftest.py +15 -0
- package/helper-apps/cortex-autogen2/tests/database/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/database/repository.py +501 -0
- package/helper-apps/cortex-autogen2/tests/database/schema.sql +108 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/llm_scorer.py +294 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/prompts.py +250 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/wordcloud_validator.py +168 -0
- package/helper-apps/cortex-autogen2/tests/metrics/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/metrics/collector.py +155 -0
- package/helper-apps/cortex-autogen2/tests/orchestrator.py +576 -0
- package/helper-apps/cortex-autogen2/tests/test_cases.yaml +279 -0
- package/helper-apps/cortex-autogen2/tests/test_data.db +0 -0
- package/helper-apps/cortex-autogen2/tests/utils/__init__.py +3 -0
- package/helper-apps/cortex-autogen2/tests/utils/connectivity.py +112 -0
- package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +74 -24
- package/helper-apps/cortex-autogen2/tools/entity_api_registry.json +38 -0
- package/helper-apps/cortex-autogen2/tools/file_tools.py +1 -1
- package/helper-apps/cortex-autogen2/tools/search_tools.py +436 -238
- package/helper-apps/cortex-file-handler/package-lock.json +2 -2
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +4 -5
- package/helper-apps/cortex-file-handler/src/blobHandler.js +36 -144
- package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +5 -3
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +34 -1
- package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +22 -0
- package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +28 -1
- package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +29 -4
- package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +11 -0
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +1 -1
- package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +3 -2
- package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +8 -1
- package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +5 -2
- package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +14 -7
- package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +5 -2
- package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +31 -19
- package/package.json +1 -1
- package/server/modelExecutor.js +4 -0
- package/server/plugins/claude4VertexPlugin.js +540 -0
- package/server/plugins/openAiWhisperPlugin.js +43 -2
- package/tests/integration/rest/vendors/claude_streaming.test.js +121 -0
- package/tests/unit/plugins/claude4VertexPlugin.test.js +462 -0
- package/tests/unit/plugins/claude4VertexToolConversion.test.js +413 -0
- package/helper-apps/cortex-autogen/.funcignore +0 -8
- package/helper-apps/cortex-autogen/Dockerfile +0 -10
- package/helper-apps/cortex-autogen/OAI_CONFIG_LIST +0 -6
- package/helper-apps/cortex-autogen/agents.py +0 -493
- package/helper-apps/cortex-autogen/agents_extra.py +0 -14
- package/helper-apps/cortex-autogen/config.py +0 -18
- package/helper-apps/cortex-autogen/data_operations.py +0 -29
- package/helper-apps/cortex-autogen/function_app.py +0 -44
- package/helper-apps/cortex-autogen/host.json +0 -15
- package/helper-apps/cortex-autogen/main.py +0 -38
- package/helper-apps/cortex-autogen/prompts.py +0 -196
- package/helper-apps/cortex-autogen/prompts_extra.py +0 -5
- package/helper-apps/cortex-autogen/requirements.txt +0 -9
- package/helper-apps/cortex-autogen/search.py +0 -85
- package/helper-apps/cortex-autogen/test.sh +0 -40
- package/helper-apps/cortex-autogen/tools/sasfileuploader.py +0 -66
- package/helper-apps/cortex-autogen/utils.py +0 -88
- package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +0 -22
- package/helper-apps/cortex-autogen2/poetry.lock +0 -3652
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker log collector for test orchestration.
|
|
3
|
+
|
|
4
|
+
Streams Docker container logs and parses them into structured format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import re
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import List, Dict, Optional
|
|
12
|
+
from collections import Counter
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LogCollector:
|
|
18
|
+
"""Streams and parses Docker container logs."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, container_name: str = "cortex-autogen-function"):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the log collector.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
container_name: Name of the Docker container to collect logs from
|
|
26
|
+
"""
|
|
27
|
+
self.container_name = container_name
|
|
28
|
+
self.logs: List[Dict] = []
|
|
29
|
+
self.is_collecting = False
|
|
30
|
+
self.process: Optional[asyncio.subprocess.Process] = None
|
|
31
|
+
|
|
32
|
+
async def start_collecting(
|
|
33
|
+
self,
|
|
34
|
+
request_id: Optional[str] = None,
|
|
35
|
+
timeout: int = 300,
|
|
36
|
+
filter_levels: Optional[List[str]] = None
|
|
37
|
+
) -> List[Dict]:
|
|
38
|
+
"""
|
|
39
|
+
Start collecting Docker logs.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
request_id: Optional request ID to filter logs for
|
|
43
|
+
timeout: Maximum time to collect in seconds
|
|
44
|
+
filter_levels: Optional list of log levels to collect (e.g., ['ERROR', 'WARNING'])
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of parsed log entries
|
|
48
|
+
"""
|
|
49
|
+
self.logs = []
|
|
50
|
+
self.is_collecting = True
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Start docker logs process
|
|
54
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
55
|
+
'docker', 'logs', '-f', '--tail=0', self.container_name,
|
|
56
|
+
stdout=asyncio.subprocess.PIPE,
|
|
57
|
+
stderr=asyncio.subprocess.PIPE
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
logger.info(f"📝 Log collector started for container: {self.container_name}")
|
|
61
|
+
if request_id:
|
|
62
|
+
logger.info(f" Filtering for request ID: {request_id}")
|
|
63
|
+
if filter_levels:
|
|
64
|
+
logger.info(f" Filtering levels: {', '.join(filter_levels)}")
|
|
65
|
+
|
|
66
|
+
# Read logs from both stdout and stderr
|
|
67
|
+
async def read_stream(stream, stream_name):
|
|
68
|
+
while self.is_collecting:
|
|
69
|
+
line = await stream.readline()
|
|
70
|
+
if not line:
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
line_str = line.decode('utf-8').strip()
|
|
75
|
+
if not line_str:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Parse the log line
|
|
79
|
+
log_entry = self._parse_log_line(line_str)
|
|
80
|
+
|
|
81
|
+
if log_entry:
|
|
82
|
+
# Apply filters
|
|
83
|
+
if request_id and request_id not in line_str:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if filter_levels and log_entry.get('level') not in filter_levels:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
self.logs.append(log_entry)
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.debug(f"Error parsing log line: {e}")
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Collect logs with timeout
|
|
96
|
+
try:
|
|
97
|
+
await asyncio.wait_for(
|
|
98
|
+
asyncio.gather(
|
|
99
|
+
read_stream(self.process.stdout, 'stdout'),
|
|
100
|
+
read_stream(self.process.stderr, 'stderr')
|
|
101
|
+
),
|
|
102
|
+
timeout=timeout
|
|
103
|
+
)
|
|
104
|
+
except asyncio.TimeoutError:
|
|
105
|
+
logger.info(f"⏱️ Log collection timeout after {timeout}s")
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"❌ Log collection error: {e}", exc_info=True)
|
|
109
|
+
finally:
|
|
110
|
+
await self.stop_collecting()
|
|
111
|
+
|
|
112
|
+
logger.info(f"📊 Log collection completed: {len(self.logs)} log entries collected")
|
|
113
|
+
return self.logs
|
|
114
|
+
|
|
115
|
+
async def stop_collecting(self):
|
|
116
|
+
"""Stop collecting logs and cleanup."""
|
|
117
|
+
self.is_collecting = False
|
|
118
|
+
|
|
119
|
+
if self.process:
|
|
120
|
+
try:
|
|
121
|
+
self.process.kill()
|
|
122
|
+
await self.process.wait()
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.debug(f"Error stopping log collection process: {e}")
|
|
125
|
+
finally:
|
|
126
|
+
self.process = None
|
|
127
|
+
|
|
128
|
+
logger.info("🛑 Log collection stopped")
|
|
129
|
+
|
|
130
|
+
def _parse_log_line(self, line: str) -> Optional[Dict]:
|
|
131
|
+
"""
|
|
132
|
+
Parse a log line into structured format.
|
|
133
|
+
|
|
134
|
+
Supports multiple log formats:
|
|
135
|
+
- Standard format: "2024-10-25 12:34:56 - INFO - [agent_name] Message"
|
|
136
|
+
- Python format: "2024-10-25 12:34:56,123 - module - INFO - Message"
|
|
137
|
+
- Simple format: "INFO: Message"
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
line: Log line string
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Parsed log entry dict or None if parsing fails
|
|
144
|
+
"""
|
|
145
|
+
# Try standard format first: "YYYY-MM-DD HH:MM:SS - LEVEL - [agent] Message"
|
|
146
|
+
pattern1 = r'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})[,\s-]+([A-Z]+)\s*-?\s*\[?([^\]]*)\]?\s*[-:]?\s*(.*)'
|
|
147
|
+
match = re.search(pattern1, line)
|
|
148
|
+
|
|
149
|
+
if match:
|
|
150
|
+
timestamp_str = match.group(1)
|
|
151
|
+
level = match.group(2).strip()
|
|
152
|
+
agent = match.group(3).strip() if match.group(3) else None
|
|
153
|
+
message = match.group(4).strip()
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
timestamp = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
|
|
157
|
+
except:
|
|
158
|
+
timestamp = datetime.now()
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
'timestamp': timestamp.isoformat(),
|
|
162
|
+
'level': level,
|
|
163
|
+
'agent': agent if agent else None,
|
|
164
|
+
'message': message,
|
|
165
|
+
'raw': line
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Try simple level format: "LEVEL: Message" or "LEVEL - Message"
|
|
169
|
+
pattern2 = r'^([A-Z]+)[\s:-]+(.+)$'
|
|
170
|
+
match = re.search(pattern2, line)
|
|
171
|
+
|
|
172
|
+
if match:
|
|
173
|
+
return {
|
|
174
|
+
'timestamp': datetime.now().isoformat(),
|
|
175
|
+
'level': match.group(1).strip(),
|
|
176
|
+
'agent': None,
|
|
177
|
+
'message': match.group(2).strip(),
|
|
178
|
+
'raw': line
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# If no pattern matches, store as unparsed
|
|
182
|
+
return {
|
|
183
|
+
'timestamp': datetime.now().isoformat(),
|
|
184
|
+
'level': 'UNKNOWN',
|
|
185
|
+
'agent': None,
|
|
186
|
+
'message': line,
|
|
187
|
+
'raw': line
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
def get_logs(
|
|
191
|
+
self,
|
|
192
|
+
level: Optional[str] = None,
|
|
193
|
+
agent: Optional[str] = None
|
|
194
|
+
) -> List[Dict]:
|
|
195
|
+
"""
|
|
196
|
+
Get collected logs with optional filtering.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
level: Filter by log level
|
|
200
|
+
agent: Filter by agent name
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Filtered list of log entries
|
|
204
|
+
"""
|
|
205
|
+
filtered = self.logs
|
|
206
|
+
|
|
207
|
+
if level:
|
|
208
|
+
filtered = [log for log in filtered if log.get('level') == level]
|
|
209
|
+
|
|
210
|
+
if agent:
|
|
211
|
+
filtered = [log for log in filtered if log.get('agent') == agent]
|
|
212
|
+
|
|
213
|
+
return filtered
|
|
214
|
+
|
|
215
|
+
def get_summary(self) -> Dict:
|
|
216
|
+
"""
|
|
217
|
+
Get a summary of collected logs.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dictionary with log statistics
|
|
221
|
+
"""
|
|
222
|
+
if not self.logs:
|
|
223
|
+
return {
|
|
224
|
+
'total_logs': 0,
|
|
225
|
+
'by_level': {},
|
|
226
|
+
'by_agent': {},
|
|
227
|
+
'errors': 0,
|
|
228
|
+
'warnings': 0
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
level_counts = Counter(log.get('level', 'UNKNOWN') for log in self.logs)
|
|
232
|
+
agent_counts = Counter(log.get('agent', 'unknown') for log in self.logs if log.get('agent'))
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
'total_logs': len(self.logs),
|
|
236
|
+
'by_level': dict(level_counts),
|
|
237
|
+
'by_agent': dict(agent_counts),
|
|
238
|
+
'errors': level_counts.get('ERROR', 0),
|
|
239
|
+
'warnings': level_counts.get('WARNING', 0) + level_counts.get('WARN', 0),
|
|
240
|
+
'first_log': self.logs[0]['timestamp'] if self.logs else None,
|
|
241
|
+
'last_log': self.logs[-1]['timestamp'] if self.logs else None
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def get_errors(self) -> List[Dict]:
|
|
245
|
+
"""Get all ERROR level logs."""
|
|
246
|
+
return self.get_logs(level='ERROR')
|
|
247
|
+
|
|
248
|
+
def get_warnings(self) -> List[Dict]:
|
|
249
|
+
"""Get all WARNING level logs."""
|
|
250
|
+
warnings = self.get_logs(level='WARNING')
|
|
251
|
+
warnings.extend(self.get_logs(level='WARN'))
|
|
252
|
+
return warnings
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Progress update collector for test orchestration.
|
|
3
|
+
|
|
4
|
+
Subscribes to Redis pub/sub channel and collects progress updates
|
|
5
|
+
during test execution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import redis
|
|
9
|
+
import json
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import List, Dict, Optional
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProgressCollector:
|
|
20
|
+
"""Collects progress updates from Redis pub/sub channel."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, redis_url: str, channel: str):
|
|
23
|
+
"""
|
|
24
|
+
Initialize the progress collector.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
redis_url: Redis connection string (e.g., "redis://localhost:6379")
|
|
28
|
+
channel: Redis channel name to subscribe to
|
|
29
|
+
"""
|
|
30
|
+
self.redis_url = redis_url
|
|
31
|
+
self.channel = channel
|
|
32
|
+
self.updates: List[Dict] = []
|
|
33
|
+
self.is_collecting = False
|
|
34
|
+
self.final_result = None
|
|
35
|
+
|
|
36
|
+
async def start_collecting(
|
|
37
|
+
self,
|
|
38
|
+
request_id: str,
|
|
39
|
+
timeout: int = 300,
|
|
40
|
+
stop_on_final: bool = True
|
|
41
|
+
) -> List[Dict]:
|
|
42
|
+
"""
|
|
43
|
+
Start collecting progress updates for a specific request.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
request_id: The request ID to filter updates for
|
|
47
|
+
timeout: Maximum time to collect in seconds
|
|
48
|
+
stop_on_final: Stop collecting when final update (progress=1.0 or data field) is received
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of progress updates collected
|
|
52
|
+
"""
|
|
53
|
+
self.updates = []
|
|
54
|
+
self.is_collecting = True
|
|
55
|
+
self.final_result = None
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
# Create Redis client in executor to avoid blocking
|
|
59
|
+
redis_client = redis.from_url(self.redis_url)
|
|
60
|
+
pubsub = redis_client.pubsub()
|
|
61
|
+
pubsub.subscribe(self.channel)
|
|
62
|
+
|
|
63
|
+
logger.info(f"📡 Progress collector started for request {request_id}")
|
|
64
|
+
logger.info(f" Subscribed to channel: {self.channel}")
|
|
65
|
+
logger.info(f" Timeout: {timeout}s")
|
|
66
|
+
|
|
67
|
+
start_time = datetime.now()
|
|
68
|
+
message_count = 0
|
|
69
|
+
|
|
70
|
+
# Listen for messages with timeout
|
|
71
|
+
for message in pubsub.listen():
|
|
72
|
+
if not self.is_collecting:
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if message['type'] == 'message':
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(message['data'])
|
|
78
|
+
|
|
79
|
+
# Only collect updates for our request
|
|
80
|
+
if data.get('requestId') == request_id:
|
|
81
|
+
message_count += 1
|
|
82
|
+
|
|
83
|
+
update = {
|
|
84
|
+
'timestamp': datetime.now().isoformat(),
|
|
85
|
+
'progress': data.get('progress', 0.0),
|
|
86
|
+
'info': data.get('info', ''),
|
|
87
|
+
'data': data.get('data')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
self.updates.append(update)
|
|
91
|
+
|
|
92
|
+
# Log progress update
|
|
93
|
+
progress_pct = int(update['progress'] * 100)
|
|
94
|
+
logger.info(f" Progress: {progress_pct}% - {update['info']}")
|
|
95
|
+
|
|
96
|
+
# Check if this is the final update
|
|
97
|
+
if stop_on_final:
|
|
98
|
+
if update['data'] is not None:
|
|
99
|
+
self.final_result = update['data']
|
|
100
|
+
logger.info(f"✅ Final result received (with data field)")
|
|
101
|
+
break
|
|
102
|
+
elif update['progress'] >= 1.0:
|
|
103
|
+
logger.info(f"✅ Final progress reached (100%)")
|
|
104
|
+
# Wait a bit more to catch any late final result
|
|
105
|
+
await asyncio.sleep(2)
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
except json.JSONDecodeError as e:
|
|
109
|
+
logger.warning(f"Failed to parse message: {e}")
|
|
110
|
+
continue
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(f"Error processing message: {e}")
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Check timeout
|
|
116
|
+
elapsed = (datetime.now() - start_time).total_seconds()
|
|
117
|
+
if elapsed > timeout:
|
|
118
|
+
logger.warning(f"⏱️ Progress collection timeout after {timeout}s")
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
# Cleanup
|
|
122
|
+
pubsub.unsubscribe()
|
|
123
|
+
pubsub.close()
|
|
124
|
+
redis_client.close()
|
|
125
|
+
|
|
126
|
+
logger.info(f"📊 Progress collection completed: {message_count} updates collected")
|
|
127
|
+
|
|
128
|
+
except redis.ConnectionError as e:
|
|
129
|
+
logger.error(f"❌ Redis connection error: {e}")
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"❌ Progress collection error: {e}", exc_info=True)
|
|
132
|
+
finally:
|
|
133
|
+
self.is_collecting = False
|
|
134
|
+
|
|
135
|
+
return self.updates
|
|
136
|
+
|
|
137
|
+
def stop_collecting(self):
|
|
138
|
+
"""Stop collecting progress updates."""
|
|
139
|
+
self.is_collecting = False
|
|
140
|
+
logger.info("🛑 Progress collection stopped manually")
|
|
141
|
+
|
|
142
|
+
def get_updates(self) -> List[Dict]:
|
|
143
|
+
"""Get all collected updates."""
|
|
144
|
+
return self.updates
|
|
145
|
+
|
|
146
|
+
def get_final_result(self) -> Optional[Dict]:
|
|
147
|
+
"""Get the final result data if received."""
|
|
148
|
+
return self.final_result
|
|
149
|
+
|
|
150
|
+
def get_summary(self) -> Dict:
|
|
151
|
+
"""
|
|
152
|
+
Get a summary of collected progress updates.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dictionary with statistics about the updates
|
|
156
|
+
"""
|
|
157
|
+
if not self.updates:
|
|
158
|
+
return {
|
|
159
|
+
'total_updates': 0,
|
|
160
|
+
'duration_seconds': 0,
|
|
161
|
+
'avg_interval_seconds': 0,
|
|
162
|
+
'final_progress': 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
timestamps = [datetime.fromisoformat(u['timestamp']) for u in self.updates]
|
|
166
|
+
intervals = []
|
|
167
|
+
|
|
168
|
+
for i in range(1, len(timestamps)):
|
|
169
|
+
interval = (timestamps[i] - timestamps[i-1]).total_seconds()
|
|
170
|
+
intervals.append(interval)
|
|
171
|
+
|
|
172
|
+
duration = (timestamps[-1] - timestamps[0]).total_seconds() if len(timestamps) > 1 else 0
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
'total_updates': len(self.updates),
|
|
176
|
+
'duration_seconds': duration,
|
|
177
|
+
'avg_interval_seconds': sum(intervals) / len(intervals) if intervals else 0,
|
|
178
|
+
'min_interval_seconds': min(intervals) if intervals else 0,
|
|
179
|
+
'max_interval_seconds': max(intervals) if intervals else 0,
|
|
180
|
+
'final_progress': self.updates[-1]['progress'] if self.updates else 0,
|
|
181
|
+
'has_final_result': self.final_result is not None
|
|
182
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pytest configuration file.
|
|
3
|
+
|
|
4
|
+
This file can be used for pytest-based testing in the future.
|
|
5
|
+
For now, use the CLI runner: python tests/cli/run_tests.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
# Configure logging for tests
|
|
12
|
+
logging.basicConfig(
|
|
13
|
+
level=logging.INFO,
|
|
14
|
+
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
15
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Database layer for test results storage."""
|