empathy-framework 4.6.3__py3-none-any.whl → 4.6.5__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 (65) hide show
  1. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/METADATA +53 -11
  2. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/RECORD +32 -57
  3. empathy_llm_toolkit/agent_factory/crews/health_check.py +7 -4
  4. empathy_llm_toolkit/agent_factory/decorators.py +3 -2
  5. empathy_llm_toolkit/agent_factory/memory_integration.py +6 -2
  6. empathy_llm_toolkit/contextual_patterns.py +5 -2
  7. empathy_llm_toolkit/git_pattern_extractor.py +8 -4
  8. empathy_llm_toolkit/providers.py +4 -3
  9. empathy_os/__init__.py +1 -1
  10. empathy_os/cli/__init__.py +306 -0
  11. empathy_os/cli/__main__.py +26 -0
  12. empathy_os/cli/commands/__init__.py +8 -0
  13. empathy_os/cli/commands/inspection.py +48 -0
  14. empathy_os/cli/commands/memory.py +56 -0
  15. empathy_os/cli/commands/provider.py +86 -0
  16. empathy_os/cli/commands/utilities.py +94 -0
  17. empathy_os/cli/core.py +32 -0
  18. empathy_os/cli.py +18 -6
  19. empathy_os/cli_unified.py +19 -3
  20. empathy_os/memory/short_term.py +12 -2
  21. empathy_os/project_index/scanner.py +151 -49
  22. empathy_os/socratic/visual_editor.py +9 -4
  23. empathy_os/workflows/bug_predict.py +70 -1
  24. empathy_os/workflows/pr_review.py +6 -0
  25. empathy_os/workflows/security_audit.py +13 -0
  26. empathy_os/workflows/tier_tracking.py +50 -2
  27. wizards/discharge_summary_wizard.py +4 -2
  28. wizards/incident_report_wizard.py +4 -2
  29. empathy_os/meta_workflows/agent_creator 2.py +0 -254
  30. empathy_os/meta_workflows/builtin_templates 2.py +0 -567
  31. empathy_os/meta_workflows/cli_meta_workflows 2.py +0 -1551
  32. empathy_os/meta_workflows/form_engine 2.py +0 -304
  33. empathy_os/meta_workflows/intent_detector 2.py +0 -298
  34. empathy_os/meta_workflows/pattern_learner 2.py +0 -754
  35. empathy_os/meta_workflows/session_context 2.py +0 -398
  36. empathy_os/meta_workflows/template_registry 2.py +0 -229
  37. empathy_os/meta_workflows/workflow 2.py +0 -980
  38. empathy_os/orchestration/pattern_learner 2.py +0 -699
  39. empathy_os/orchestration/real_tools 2.py +0 -938
  40. empathy_os/socratic/__init__ 2.py +0 -273
  41. empathy_os/socratic/ab_testing 2.py +0 -969
  42. empathy_os/socratic/blueprint 2.py +0 -532
  43. empathy_os/socratic/cli 2.py +0 -689
  44. empathy_os/socratic/collaboration 2.py +0 -1112
  45. empathy_os/socratic/domain_templates 2.py +0 -916
  46. empathy_os/socratic/embeddings 2.py +0 -734
  47. empathy_os/socratic/engine 2.py +0 -729
  48. empathy_os/socratic/explainer 2.py +0 -663
  49. empathy_os/socratic/feedback 2.py +0 -767
  50. empathy_os/socratic/forms 2.py +0 -624
  51. empathy_os/socratic/generator 2.py +0 -716
  52. empathy_os/socratic/llm_analyzer 2.py +0 -635
  53. empathy_os/socratic/mcp_server 2.py +0 -751
  54. empathy_os/socratic/session 2.py +0 -306
  55. empathy_os/socratic/storage 2.py +0 -635
  56. empathy_os/socratic/success 2.py +0 -719
  57. empathy_os/socratic/visual_editor 2.py +0 -812
  58. empathy_os/socratic/web_ui 2.py +0 -925
  59. empathy_os/workflows/batch_processing 2.py +0 -310
  60. empathy_os/workflows/release_prep_crew 2.py +0 -968
  61. empathy_os/workflows/test_coverage_boost_crew 2.py +0 -848
  62. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/WHEEL +0 -0
  63. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/entry_points.txt +0 -0
  64. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/licenses/LICENSE +0 -0
  65. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/top_level.txt +0 -0
@@ -1,848 +0,0 @@
1
- """Test Coverage Boost Crew - Multi-agent test generation workflow.
2
-
3
- .. deprecated:: 4.3.0
4
- This workflow is deprecated in favor of the meta-workflow system.
5
- Use ``empathy meta-workflow run test-coverage-boost`` instead.
6
- See docs/CREWAI_MIGRATION.md for migration guide.
7
-
8
- This module provides a CrewAI-based workflow that uses 3 specialized agents
9
- to analyze coverage gaps, generate tests, and validate improvements.
10
-
11
- Copyright 2025 Smart AI Memory, LLC
12
- Licensed under Fair Source 0.9
13
- """
14
- import asyncio
15
- import json
16
- import re
17
- import warnings
18
- from dataclasses import dataclass, field
19
- from datetime import datetime
20
- from pathlib import Path
21
-
22
- from empathy_os.models.executor import ExecutionContext
23
-
24
-
25
- @dataclass
26
- class Agent:
27
- """Agent configuration for test coverage boost."""
28
-
29
- role: str
30
- goal: str
31
- backstory: str
32
- expertise_level: str = "expert"
33
- weight: float = 1.0
34
-
35
- def get_system_prompt(self) -> str:
36
- """Generate system prompt for this agent."""
37
- return f"""You are a {self.role}.
38
-
39
- {self.backstory}
40
-
41
- Your goal: {self.goal}
42
-
43
- Expertise level: {self.expertise_level}
44
-
45
- Provide your response in this format:
46
- <thinking>
47
- [Your analysis and reasoning]
48
- </thinking>
49
-
50
- <answer>
51
- [Your JSON response matching the expected format]
52
- </answer>"""
53
-
54
-
55
- @dataclass
56
- class Task:
57
- """Task configuration for an agent."""
58
-
59
- description: str
60
- expected_output: str
61
- context_keys: list[str] = field(default_factory=list)
62
-
63
- def get_user_prompt(self, context: dict) -> str:
64
- """Generate user prompt with context data."""
65
- # Build context section
66
- context_lines = ["<context>"]
67
- for key in self.context_keys:
68
- if key in context:
69
- value = context[key]
70
- if isinstance(value, (dict, list)):
71
- value = json.dumps(value, indent=2)
72
- context_lines.append(f"<{key}>")
73
- context_lines.append(str(value))
74
- context_lines.append(f"</{key}>")
75
- context_lines.append("</context>")
76
-
77
- return f"""{self.description}
78
-
79
- {chr(10).join(context_lines)}
80
-
81
- <expected_output>
82
- {self.expected_output}
83
- </expected_output>
84
-
85
- <instructions>
86
- 1. Review all context data in the <context> tags above
87
- 2. Structure your response using <thinking> and <answer> tags
88
- 3. Provide a JSON response in the <answer> section matching the expected output format exactly
89
- </instructions>"""
90
-
91
-
92
- @dataclass
93
- class CoverageGap:
94
- """Represents a gap in test coverage."""
95
-
96
- file_path: str
97
- function_name: str
98
- line_start: int
99
- line_end: int
100
- priority: float # 0-1, higher = more important
101
- reason: str
102
-
103
-
104
- @dataclass
105
- class GeneratedTest:
106
- """Represents a generated test case."""
107
-
108
- test_name: str
109
- test_code: str
110
- target_function: str
111
- target_file: str
112
- coverage_impact: float # Estimated coverage improvement
113
-
114
-
115
- @dataclass
116
- class TestCoverageBoostCrewResult:
117
- """Result from TestCoverageBoostCrew execution."""
118
-
119
- success: bool
120
- current_coverage: float # 0-100
121
- target_coverage: float # 0-100
122
- final_coverage: float # 0-100
123
- coverage_improvement: float # Percentage points gained
124
-
125
- # Detailed results
126
- gaps_found: int
127
- tests_generated: int
128
- tests_passing: int
129
- gaps_analyzed: list[CoverageGap] = field(default_factory=list)
130
- generated_tests: list[GeneratedTest] = field(default_factory=list)
131
-
132
- # Execution metadata
133
- agents_executed: int = 3
134
- cost: float = 0.0
135
- duration_ms: int = 0
136
- timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
137
- errors: list[str] = field(default_factory=list)
138
-
139
-
140
- def parse_xml_response(response: str) -> dict:
141
- """Parse XML-structured agent response."""
142
- result = {
143
- "thinking": "",
144
- "answer": "",
145
- "raw": response,
146
- "has_xml_structure": False,
147
- }
148
-
149
- # Try to extract thinking section
150
- thinking_start = response.find("<thinking>")
151
- thinking_end = response.find("</thinking>")
152
- if thinking_start != -1 and thinking_end != -1:
153
- result["thinking"] = response[thinking_start + 10 : thinking_end].strip()
154
- result["has_xml_structure"] = True
155
-
156
- # Try to extract answer section
157
- answer_start = response.find("<answer>")
158
- answer_end = response.find("</answer>")
159
- if answer_start != -1 and answer_end != -1:
160
- result["answer"] = response[answer_start + 8 : answer_end].strip()
161
- result["has_xml_structure"] = True
162
-
163
- # If no answer found, extract content after </thinking> or use full response
164
- if not result["answer"]:
165
- if thinking_end != -1:
166
- # Extract everything after </thinking> tag
167
- result["answer"] = response[thinking_end + 11 :].strip()
168
- else:
169
- # Use full response as fallback
170
- result["answer"] = response
171
-
172
- return result
173
-
174
-
175
- class TestCoverageBoostCrew:
176
- """Test Coverage Boost Crew - Multi-agent test generation.
177
-
178
- Uses 3 specialized agents to analyze coverage gaps, generate tests,
179
- and validate improvements.
180
-
181
- Agents:
182
- - Gap Analyzer: Identifies untested code and prioritizes gaps
183
- - Test Generator: Creates comprehensive test cases
184
- - Test Validator: Validates generated tests and measures improvement
185
-
186
- Usage:
187
- crew = TestCoverageBoostCrew(target_coverage=85.0)
188
- result = await crew.execute(project_root="./src")
189
-
190
- print(f"Coverage improved by {result.coverage_improvement}%")
191
- """
192
-
193
- name = "Test_Coverage_Boost_Crew"
194
- description = "Multi-agent test generation with gap analysis and validation"
195
- process_type = "sequential"
196
-
197
- def __init__(
198
- self,
199
- target_coverage: float = 80.0,
200
- project_root: str = ".",
201
- **kwargs, # Accept extra CLI arguments
202
- ):
203
- """Initialize the test coverage boost crew.
204
-
205
- .. deprecated:: 4.3.0
206
- Use meta-workflow system instead: ``empathy meta-workflow run test-coverage-boost``
207
-
208
- Args:
209
- target_coverage: Target coverage percentage (0-100)
210
- project_root: Root directory of project to analyze
211
- **kwargs: Additional arguments (ignored, for CLI compatibility)
212
- """
213
- warnings.warn(
214
- "TestCoverageBoostCrew is deprecated since v4.3.0. "
215
- "Use meta-workflow system instead: empathy meta-workflow run test-coverage-boost. "
216
- "See docs/CREWAI_MIGRATION.md for migration guide.",
217
- DeprecationWarning,
218
- stacklevel=2,
219
- )
220
- if not 0 <= target_coverage <= 100:
221
- raise ValueError("target_coverage must be between 0 and 100")
222
-
223
- self.target_coverage = target_coverage
224
- self.project_root = Path(project_root).resolve()
225
-
226
- # Initialize tracking
227
- self._total_cost = 0.0
228
- self._total_input_tokens = 0
229
- self._total_output_tokens = 0
230
- self._executor = None
231
- self._project_index = None
232
-
233
- # Initialize ProjectIndex if available
234
- try:
235
- from empathy_os.project_index import ProjectIndex
236
-
237
- self._project_index = ProjectIndex(str(self.project_root))
238
- if not self._project_index.load():
239
- print(" [ProjectIndex] Building index (first run)...")
240
- self._project_index.refresh()
241
- else:
242
- print(" [ProjectIndex] Loaded existing index")
243
- except Exception as e:
244
- print(f" [ProjectIndex] Warning: Could not load index: {e}")
245
- self._project_index = None
246
-
247
- # Define agents
248
- self.agents = [
249
- Agent(
250
- role="Gap Analyzer",
251
- goal="Identify critical gaps in test coverage and prioritize them",
252
- backstory="Expert code analyzer specializing in identifying untested code paths. "
253
- "You understand which functions are most critical to test based on complexity, "
254
- "usage patterns, and risk. You prioritize gaps by impact and provide actionable insights.",
255
- expertise_level="expert",
256
- ),
257
- Agent(
258
- role="Test Generator",
259
- goal="Generate comprehensive, high-quality test cases for coverage gaps",
260
- backstory="Senior test engineer who writes clean, maintainable, effective tests. "
261
- "You follow testing best practices, use appropriate assertions, cover edge cases, "
262
- "and write tests that are both thorough and readable.",
263
- expertise_level="expert",
264
- ),
265
- Agent(
266
- role="Test Validator",
267
- goal="Validate generated tests and measure coverage improvement",
268
- backstory="QA specialist focused on test quality and coverage metrics. "
269
- "You verify that tests are correct, run successfully, and actually improve coverage. "
270
- "You identify issues with generated tests and recommend fixes.",
271
- expertise_level="expert",
272
- ),
273
- ]
274
-
275
- def _initialize_executor(self):
276
- """Initialize LLM executor for agent calls."""
277
- if self._executor is not None:
278
- return
279
-
280
- try:
281
- from empathy_os.models.empathy_executor import EmpathyLLMExecutor
282
-
283
- self._executor = EmpathyLLMExecutor(provider="anthropic")
284
- except Exception as e:
285
- print(f" [LLM] Warning: Could not initialize executor: {e}")
286
- print(" [LLM] Workflow will use mock responses")
287
- self._executor = None
288
-
289
- def define_tasks(self) -> list[Task]:
290
- """Define tasks for each agent."""
291
- return [
292
- Task(
293
- description="Analyze the codebase and identify critical test coverage gaps",
294
- expected_output="""JSON object with:
295
- {
296
- "gaps": [
297
- {
298
- "file_path": "path/to/file.py",
299
- "function": "function_name",
300
- "line_start": 10,
301
- "line_end": 50,
302
- "priority": 0.9,
303
- "reason": "High complexity function with no tests"
304
- }
305
- ],
306
- "current_coverage": 65.0,
307
- "summary": "Found 5 critical gaps in high-impact files"
308
- }""",
309
- context_keys=[
310
- "project_root",
311
- "target_coverage",
312
- "project_stats",
313
- "coverage_data",
314
- "files_to_analyze",
315
- "high_impact_files",
316
- ],
317
- ),
318
- Task(
319
- description="Generate comprehensive test cases for the identified coverage gaps",
320
- expected_output="""JSON object with properly escaped strings:
321
- {
322
- "tests": [
323
- {
324
- "test_name": "test_function_name_edge_case",
325
- "test_code": "def test_function_name_edge_case():\\n assert result == \\"expected\\"\\n assert x != \\"bad\\"",
326
- "target_function": "function_name",
327
- "target_file": "path/to/file.py",
328
- "coverage_impact": 5.2
329
- }
330
- ],
331
- "total_tests": 5,
332
- "estimated_coverage_gain": 12.5
333
- }
334
-
335
- CRITICAL FORMATTING RULES:
336
- 1. ALWAYS escape quotes in test_code: Use \\" not "
337
- 2. Use \\n for newlines in test_code
338
- 3. Example CORRECT: "test_code": "def test():\\n assert x == \\"value\\""
339
- 4. Example WRONG: "test_code": "def test(): assert x == "value""
340
- 5. Keep test_code concise - max 5 lines per test""",
341
- context_keys=["gaps", "project_root", "existing_tests"],
342
- ),
343
- Task(
344
- description="Validate the generated tests and measure actual coverage improvement",
345
- expected_output="""JSON object with:
346
- {
347
- "tests_passing": 4,
348
- "tests_failing": 1,
349
- "final_coverage": 77.5,
350
- "coverage_improvement": 12.5,
351
- "issues": ["test_foo failed: assertion error"],
352
- "recommendations": ["Add fixture for database setup"]
353
- }""",
354
- context_keys=["generated_tests", "target_coverage"],
355
- ),
356
- ]
357
-
358
- def _get_file_contents_for_analysis(self, files: list) -> list[dict]:
359
- """Read actual file contents for analysis.
360
-
361
- Args:
362
- files: List of FileRecord objects
363
-
364
- Returns:
365
- List of dicts with path, code, and metadata
366
- """
367
- result = []
368
- for file in files:
369
- try:
370
- file_path = self.project_root / file.path
371
- if not file_path.exists() or not file_path.suffix == ".py":
372
- continue
373
-
374
- code = file_path.read_text(encoding="utf-8")
375
-
376
- # Limit code size to avoid token bloat (max ~5000 chars per file)
377
- if len(code) > 5000:
378
- code = code[:5000] + f"\n... (truncated, {len(code)-5000} more chars)"
379
-
380
- result.append(
381
- {
382
- "path": str(file.path),
383
- "complexity": file.complexity_score,
384
- "lines": file.lines_of_code,
385
- "has_test": file.tests_exist,
386
- "coverage": file.coverage_percent,
387
- "code": code,
388
- }
389
- )
390
- except Exception:
391
- # Skip files that can't be read
392
- continue
393
-
394
- return result
395
-
396
- def _get_project_context(self) -> dict:
397
- """Get project context from ProjectIndex."""
398
- if self._project_index is None:
399
- return {
400
- "project_root": str(self.project_root),
401
- "target_coverage": self.target_coverage,
402
- }
403
-
404
- try:
405
- summary = self._project_index.get_summary()
406
-
407
- # Get files needing tests for gap analysis
408
- files_needing_tests = self._project_index.get_files_needing_tests()
409
-
410
- # Get high impact files for prioritization
411
- high_impact_files = self._project_index.get_high_impact_files()
412
-
413
- return {
414
- "project_root": str(self.project_root),
415
- "target_coverage": self.target_coverage,
416
- "project_stats": {
417
- "total_files": summary.total_files,
418
- "source_files": summary.source_files,
419
- "test_files": summary.test_files,
420
- "total_loc": summary.total_lines_of_code,
421
- "avg_complexity": summary.avg_complexity,
422
- "test_coverage_avg": summary.test_coverage_avg,
423
- },
424
- "coverage_data": {
425
- "current_coverage": summary.test_coverage_avg,
426
- "files_without_tests": summary.files_without_tests,
427
- "files_needing_tests": len(files_needing_tests),
428
- },
429
- "files_to_analyze": self._get_file_contents_for_analysis(
430
- files_needing_tests[:5]
431
- ), # Top 5 files with code
432
- "high_impact_files": [
433
- {
434
- "path": str(file.path),
435
- "impact_score": file.impact_score,
436
- "complexity": file.complexity_score,
437
- "lines": file.lines_of_code,
438
- }
439
- for file in high_impact_files[:5] # Top 5 high-impact files
440
- ],
441
- }
442
- except Exception as e:
443
- print(f" [ProjectIndex] Could not load project data: {e}")
444
- return {
445
- "project_root": str(self.project_root),
446
- "target_coverage": self.target_coverage,
447
- }
448
-
449
- async def _call_llm(
450
- self,
451
- agent: Agent,
452
- task: Task,
453
- context: dict,
454
- ) -> tuple[str, int, int, float]:
455
- """Call the LLM with agent/task configuration.
456
-
457
- Returns: (response_text, input_tokens, output_tokens, cost)
458
- """
459
- system_prompt = agent.get_system_prompt()
460
- user_prompt = task.get_user_prompt(context)
461
-
462
- if self._executor is None:
463
- # Fallback: return mock response
464
- return await self._mock_llm_call(agent, task)
465
-
466
- try:
467
- # Create execution context
468
- exec_context = ExecutionContext(
469
- task_type="test_generation",
470
- workflow_name="test-coverage-boost",
471
- step_name=agent.role,
472
- )
473
-
474
- # Execute with timeout using correct LLMExecutor API
475
- result = await asyncio.wait_for(
476
- self._executor.run(
477
- task_type="test_generation",
478
- prompt=user_prompt,
479
- system=system_prompt,
480
- context=exec_context,
481
- ),
482
- timeout=120.0,
483
- )
484
-
485
- response = result.content
486
- input_tokens = result.input_tokens
487
- output_tokens = result.output_tokens
488
- cost = result.cost
489
-
490
- # Track totals
491
- self._total_cost += cost
492
- self._total_input_tokens += input_tokens
493
- self._total_output_tokens += output_tokens
494
-
495
- return (response, input_tokens, output_tokens, cost)
496
-
497
- except asyncio.TimeoutError:
498
- print(f" [LLM] Timeout calling {agent.role}")
499
- return await self._mock_llm_call(agent, task, reason="Timeout")
500
- except Exception as e:
501
- print(f" [LLM] Error calling {agent.role}: {e}")
502
- return await self._mock_llm_call(agent, task, reason=str(e))
503
-
504
- async def _mock_llm_call(
505
- self,
506
- agent: Agent,
507
- task: Task,
508
- reason: str = "Executor not available",
509
- ) -> tuple[str, int, int, float]:
510
- """Return mock response when LLM is unavailable."""
511
- print(f" [LLM] Using mock response for {agent.role}: {reason}")
512
-
513
- # Simple mock responses based on agent role
514
- if "Gap Analyzer" in agent.role:
515
- response = json.dumps(
516
- {
517
- "gaps": [
518
- {
519
- "file_path": "src/core.py",
520
- "function": "process_data",
521
- "line_start": 10,
522
- "line_end": 50,
523
- "priority": 0.9,
524
- "reason": "High complexity function with no tests",
525
- }
526
- ],
527
- "current_coverage": 65.0,
528
- "summary": "Mock response - no real gaps analyzed",
529
- }
530
- )
531
- elif "Test Generator" in agent.role:
532
- response = json.dumps({"tests": [], "total_tests": 0, "estimated_coverage_gain": 0.0})
533
- else: # Validator
534
- response = json.dumps(
535
- {
536
- "tests_passing": 0,
537
- "tests_failing": 0,
538
- "final_coverage": 65.0,
539
- "coverage_improvement": 0.0,
540
- "issues": ["Mock response - no real validation performed"],
541
- "recommendations": [],
542
- }
543
- )
544
-
545
- return (response, 0, 0, 0.0)
546
-
547
- async def execute(
548
- self,
549
- project_root: str | None = None,
550
- context: dict | None = None,
551
- **kwargs, # Accept extra parameters from CLI
552
- ) -> TestCoverageBoostCrewResult:
553
- """Execute the test coverage boost crew.
554
-
555
- Args:
556
- project_root: Path to project root (overrides init value)
557
- context: Additional context for agents
558
- **kwargs: Additional arguments (e.g., target_coverage passed by CLI)
559
-
560
- Returns:
561
- TestCoverageBoostCrewResult with detailed outcomes
562
- """
563
- if project_root:
564
- self.project_root = Path(project_root).resolve()
565
-
566
- # Merge kwargs into context for CLI compatibility
567
- context = context or {}
568
- context.update(kwargs)
569
-
570
- started_at = datetime.now()
571
-
572
- print("\n" + "=" * 70)
573
- print(" TEST COVERAGE BOOST CREW")
574
- print("=" * 70)
575
- print(f"\n Project Root: {self.project_root}")
576
- print(f" Target Coverage: {self.target_coverage}%")
577
- print(f" Agents: {len(self.agents)} (sequential execution)")
578
- print("")
579
-
580
- # Initialize executor
581
- self._initialize_executor()
582
-
583
- # Get project context
584
- agent_context = self._get_project_context()
585
- agent_context.update(context)
586
-
587
- # Define tasks
588
- tasks = self.define_tasks()
589
-
590
- # Execute agents sequentially, passing results forward
591
- print(" 🚀 Executing agents sequentially...\n")
592
-
593
- # Agent 1: Gap Analyzer
594
- print(f" • {self.agents[0].role}")
595
- gap_response, _, _, _ = await self._call_llm(self.agents[0], tasks[0], agent_context)
596
- gap_data = self._parse_gap_analysis(gap_response)
597
- agent_context["gaps"] = gap_data.get("gaps", [])
598
- agent_context["current_coverage"] = gap_data.get("current_coverage", 0.0)
599
-
600
- # Agent 2: Test Generator
601
- print(f" • {self.agents[1].role}")
602
- gen_response, _, _, _ = await self._call_llm(self.agents[1], tasks[1], agent_context)
603
- test_data = self._parse_test_generation(gen_response)
604
- agent_context["generated_tests"] = test_data.get("tests", [])
605
-
606
- # Agent 3: Test Validator
607
- print(f" • {self.agents[2].role}")
608
- val_response, _, _, _ = await self._call_llm(self.agents[2], tasks[2], agent_context)
609
- validation_data = self._parse_validation(val_response)
610
-
611
- print("\n ✓ All agents completed\n")
612
-
613
- # Build result
614
- current_coverage = gap_data.get("current_coverage", 0.0)
615
- final_coverage = validation_data.get("final_coverage", current_coverage)
616
- coverage_improvement = final_coverage - current_coverage
617
-
618
- # Parse gaps into CoverageGap objects
619
- gaps_analyzed = []
620
- for gap in gap_data.get("gaps", [])[:5]: # Top 5 gaps
621
- gaps_analyzed.append(
622
- CoverageGap(
623
- file_path=gap.get("file_path", "unknown"),
624
- function_name=gap.get("function", "unknown"),
625
- line_start=gap.get("line_start", 0),
626
- line_end=gap.get("line_end", 0),
627
- priority=gap.get("priority", 0.5),
628
- reason=gap.get("reason", "No reason provided"),
629
- )
630
- )
631
-
632
- # Parse generated tests into GeneratedTest objects
633
- generated_tests = []
634
- for test in test_data.get("tests", []):
635
- generated_tests.append(
636
- GeneratedTest(
637
- test_name=test.get("test_name", "test_unknown"),
638
- test_code=test.get("test_code", ""),
639
- target_function=test.get("target_function", "unknown"),
640
- target_file=test.get("target_file", "unknown"),
641
- coverage_impact=test.get("coverage_impact", 0.0),
642
- )
643
- )
644
-
645
- # Calculate duration
646
- duration_ms = int((datetime.now() - started_at).total_seconds() * 1000)
647
-
648
- result = TestCoverageBoostCrewResult(
649
- success=True,
650
- current_coverage=current_coverage,
651
- target_coverage=self.target_coverage,
652
- final_coverage=final_coverage,
653
- coverage_improvement=coverage_improvement,
654
- gaps_found=len(gap_data.get("gaps", [])),
655
- tests_generated=test_data.get("total_tests", 0),
656
- tests_passing=validation_data.get("tests_passing", 0),
657
- gaps_analyzed=gaps_analyzed,
658
- generated_tests=generated_tests,
659
- agents_executed=len(self.agents),
660
- cost=self._total_cost,
661
- duration_ms=duration_ms,
662
- )
663
-
664
- # Print formatted report
665
- print(self._format_report(result))
666
-
667
- return result
668
-
669
- def _parse_gap_analysis(self, response: str) -> dict:
670
- """Parse gap analysis response."""
671
- parsed = parse_xml_response(response)
672
- answer = parsed["answer"]
673
-
674
- # Clean up answer - strip ALL XML tags and code blocks
675
- answer = re.sub(r"</?answer>", "", answer) # Remove all <answer> and </answer> tags
676
- answer = re.sub(r"```json\s*", "", answer) # Remove ```json
677
- answer = re.sub(r"```\s*", "", answer) # Remove closing ```
678
- answer = answer.strip()
679
-
680
- try:
681
- data = json.loads(answer)
682
- return data
683
- except json.JSONDecodeError:
684
- # Try regex extraction
685
- gaps = []
686
- current_coverage = 0.0
687
-
688
- # Extract coverage
689
- cov_match = re.search(r'"current_coverage"\s*:\s*(\d+\.?\d*)', answer)
690
- if cov_match:
691
- current_coverage = float(cov_match.group(1))
692
-
693
- return {
694
- "gaps": gaps,
695
- "current_coverage": current_coverage,
696
- "summary": "Could not parse gap analysis",
697
- }
698
-
699
- def _parse_test_generation(self, response: str) -> dict:
700
- """Parse test generation response."""
701
- parsed = parse_xml_response(response)
702
- answer = parsed["answer"]
703
-
704
- # Clean up answer - strip ALL XML tags and code blocks
705
- answer = re.sub(r"</?answer>", "", answer) # Remove all <answer> and </answer> tags
706
- answer = re.sub(r"```json\s*", "", answer) # Remove ```json
707
- answer = re.sub(r"```\s*", "", answer) # Remove closing ```
708
- answer = answer.strip()
709
-
710
- try:
711
- data = json.loads(answer)
712
- return data
713
- except json.JSONDecodeError:
714
- # JSON parsing failed - attempt regex extraction
715
- tests = []
716
-
717
- # Pattern to extract test objects (handles malformed JSON)
718
- # More lenient pattern - looks for key fields in any order
719
- test_blocks = re.finditer(
720
- r'\{[^}]*"test_name"\s*:\s*"([^"]+)"[^}]*\}', answer, re.DOTALL
721
- )
722
-
723
- for match in test_blocks:
724
- block_text = match.group(0)
725
-
726
- # Extract test_name
727
- test_name_match = re.search(r'"test_name"\s*:\s*"([^"]+)"', block_text)
728
- test_name = test_name_match.group(1) if test_name_match else "test_unknown"
729
-
730
- # Extract test_code (handles escaped quotes)
731
- test_code_match = re.search(
732
- r'"test_code"\s*:\s*"((?:[^"\\]|\\.)*?)"', block_text, re.DOTALL
733
- )
734
- test_code = test_code_match.group(1) if test_code_match else ""
735
-
736
- # Extract target_function
737
- func_match = re.search(r'"target_function"\s*:\s*"([^"]*)"', block_text)
738
- target_function = func_match.group(1) if func_match else "unknown"
739
-
740
- # Extract target_file
741
- file_match = re.search(r'"target_file"\s*:\s*"([^"]*)"', block_text)
742
- target_file = file_match.group(1) if file_match else "unknown"
743
-
744
- # Unescape the test code
745
- test_code = test_code.replace("\\n", "\n").replace('\\"', '"').replace("\\\\", "\\")
746
-
747
- tests.append(
748
- {
749
- "test_name": test_name,
750
- "test_code": test_code,
751
- "target_function": target_function,
752
- "target_file": target_file,
753
- "coverage_impact": 0.0,
754
- }
755
- )
756
-
757
- # Try to extract total_tests
758
- total_tests = len(tests)
759
- total_match = re.search(r'"total_tests"\s*:\s*(\d+)', answer)
760
- if total_match:
761
- total_tests = max(total_tests, int(total_match.group(1)))
762
-
763
- # Extract estimated coverage gain
764
- coverage_gain = 0.0
765
- gain_match = re.search(r'"estimated_coverage_gain"\s*:\s*(\d+\.?\d*)', answer)
766
- if gain_match:
767
- coverage_gain = float(gain_match.group(1))
768
-
769
- return {
770
- "tests": tests,
771
- "total_tests": total_tests,
772
- "estimated_coverage_gain": coverage_gain,
773
- }
774
-
775
- def _parse_validation(self, response: str) -> dict:
776
- """Parse validation response."""
777
- parsed = parse_xml_response(response)
778
- answer = parsed["answer"]
779
-
780
- # Clean up answer - strip ALL XML tags and code blocks
781
- answer = re.sub(r"</?answer>", "", answer) # Remove all <answer> and </answer> tags
782
- answer = re.sub(r"```json\s*", "", answer) # Remove ```json
783
- answer = re.sub(r"```\s*", "", answer) # Remove closing ```
784
- answer = answer.strip()
785
-
786
- try:
787
- data = json.loads(answer)
788
- return data
789
- except json.JSONDecodeError:
790
- return {
791
- "tests_passing": 0,
792
- "tests_failing": 0,
793
- "final_coverage": 0.0,
794
- "coverage_improvement": 0.0,
795
- "issues": ["Could not parse validation results"],
796
- "recommendations": [],
797
- }
798
-
799
- def _format_report(self, result: TestCoverageBoostCrewResult) -> str:
800
- """Format result as human-readable report."""
801
- lines = []
802
-
803
- lines.append("=" * 70)
804
- lines.append("TEST COVERAGE BOOST RESULTS")
805
- lines.append("=" * 70)
806
- lines.append("")
807
- lines.append(f"Current Coverage: {result.current_coverage:.1f}%")
808
- lines.append(f"Target Coverage: {result.target_coverage:.1f}%")
809
- lines.append(f"Final Coverage: {result.final_coverage:.1f}%")
810
- lines.append(f"Improvement: +{result.coverage_improvement:.1f}%")
811
- lines.append("")
812
- lines.append(f"Gaps Found: {result.gaps_found}")
813
- lines.append(f"Tests Generated: {result.tests_generated}")
814
- lines.append(f"Tests Passing: {result.tests_passing}")
815
- lines.append("")
816
- lines.append(f"Cost: ${result.cost:.4f}")
817
- lines.append(f"Duration: {result.duration_ms}ms ({result.duration_ms/1000:.1f}s)")
818
- lines.append("")
819
-
820
- if result.gaps_analyzed:
821
- lines.append("-" * 70)
822
- lines.append("TOP COVERAGE GAPS")
823
- lines.append("-" * 70)
824
- for i, gap in enumerate(result.gaps_analyzed[:5], 1):
825
- lines.append(
826
- f"{i}. {gap.file_path}::{gap.function_name} (priority: {gap.priority:.2f})"
827
- )
828
- lines.append(f" {gap.reason}")
829
- lines.append("")
830
-
831
- if result.generated_tests:
832
- lines.append("-" * 70)
833
- lines.append("GENERATED TESTS")
834
- lines.append("-" * 70)
835
- for i, test in enumerate(result.generated_tests[:3], 1):
836
- lines.append(f"{i}. {test.test_name}")
837
- lines.append(f" Target: {test.target_file}::{test.target_function}")
838
- lines.append(f" Impact: +{test.coverage_impact:.1f}%")
839
- lines.append("")
840
-
841
- lines.append("=" * 70)
842
- if result.coverage_improvement > 0:
843
- lines.append(f"✅ Coverage improved by {result.coverage_improvement:.1f}%")
844
- else:
845
- lines.append("⚠️ No coverage improvement achieved")
846
- lines.append("=" * 70)
847
-
848
- return "\n".join(lines)