empathy-framework 3.7.0__py3-none-any.whl → 3.8.0__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.
- coach_wizards/code_reviewer_README.md +60 -0
- coach_wizards/code_reviewer_wizard.py +180 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/METADATA +148 -11
- empathy_framework-3.8.0.dist-info/RECORD +333 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/top_level.txt +5 -1
- empathy_healthcare_plugin/monitors/__init__.py +9 -0
- empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py +315 -0
- empathy_healthcare_plugin/monitors/monitoring/__init__.py +44 -0
- empathy_healthcare_plugin/monitors/monitoring/protocol_checker.py +300 -0
- empathy_healthcare_plugin/monitors/monitoring/protocol_loader.py +214 -0
- empathy_healthcare_plugin/monitors/monitoring/sensor_parsers.py +306 -0
- empathy_healthcare_plugin/monitors/monitoring/trajectory_analyzer.py +389 -0
- empathy_llm_toolkit/agent_factory/__init__.py +53 -0
- empathy_llm_toolkit/agent_factory/adapters/__init__.py +85 -0
- empathy_llm_toolkit/agent_factory/adapters/autogen_adapter.py +312 -0
- empathy_llm_toolkit/agent_factory/adapters/crewai_adapter.py +454 -0
- empathy_llm_toolkit/agent_factory/adapters/haystack_adapter.py +298 -0
- empathy_llm_toolkit/agent_factory/adapters/langchain_adapter.py +362 -0
- empathy_llm_toolkit/agent_factory/adapters/langgraph_adapter.py +333 -0
- empathy_llm_toolkit/agent_factory/adapters/native.py +228 -0
- empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +426 -0
- empathy_llm_toolkit/agent_factory/base.py +305 -0
- empathy_llm_toolkit/agent_factory/crews/__init__.py +67 -0
- empathy_llm_toolkit/agent_factory/crews/code_review.py +1113 -0
- empathy_llm_toolkit/agent_factory/crews/health_check.py +1246 -0
- empathy_llm_toolkit/agent_factory/crews/refactoring.py +1128 -0
- empathy_llm_toolkit/agent_factory/crews/security_audit.py +1018 -0
- empathy_llm_toolkit/agent_factory/decorators.py +286 -0
- empathy_llm_toolkit/agent_factory/factory.py +558 -0
- empathy_llm_toolkit/agent_factory/framework.py +192 -0
- empathy_llm_toolkit/agent_factory/memory_integration.py +324 -0
- empathy_llm_toolkit/agent_factory/resilient.py +320 -0
- empathy_llm_toolkit/cli/__init__.py +8 -0
- empathy_llm_toolkit/cli/sync_claude.py +487 -0
- empathy_llm_toolkit/code_health.py +150 -3
- empathy_llm_toolkit/config/__init__.py +29 -0
- empathy_llm_toolkit/config/unified.py +295 -0
- empathy_llm_toolkit/routing/__init__.py +32 -0
- empathy_llm_toolkit/routing/model_router.py +362 -0
- empathy_llm_toolkit/security/IMPLEMENTATION_SUMMARY.md +413 -0
- empathy_llm_toolkit/security/PHASE2_COMPLETE.md +384 -0
- empathy_llm_toolkit/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- empathy_llm_toolkit/security/QUICK_REFERENCE.md +316 -0
- empathy_llm_toolkit/security/README.md +262 -0
- empathy_llm_toolkit/security/__init__.py +62 -0
- empathy_llm_toolkit/security/audit_logger.py +929 -0
- empathy_llm_toolkit/security/audit_logger_example.py +152 -0
- empathy_llm_toolkit/security/pii_scrubber.py +640 -0
- empathy_llm_toolkit/security/secrets_detector.py +678 -0
- empathy_llm_toolkit/security/secrets_detector_example.py +304 -0
- empathy_llm_toolkit/security/secure_memdocs.py +1192 -0
- empathy_llm_toolkit/security/secure_memdocs_example.py +278 -0
- empathy_llm_toolkit/wizards/__init__.py +38 -0
- empathy_llm_toolkit/wizards/base_wizard.py +364 -0
- empathy_llm_toolkit/wizards/customer_support_wizard.py +190 -0
- empathy_llm_toolkit/wizards/healthcare_wizard.py +362 -0
- empathy_llm_toolkit/wizards/patient_assessment_README.md +64 -0
- empathy_llm_toolkit/wizards/patient_assessment_wizard.py +193 -0
- empathy_llm_toolkit/wizards/technology_wizard.py +194 -0
- empathy_os/__init__.py +52 -52
- empathy_os/adaptive/__init__.py +13 -0
- empathy_os/adaptive/task_complexity.py +127 -0
- empathy_os/cache/__init__.py +117 -0
- empathy_os/cache/base.py +166 -0
- empathy_os/cache/dependency_manager.py +253 -0
- empathy_os/cache/hash_only.py +248 -0
- empathy_os/cache/hybrid.py +390 -0
- empathy_os/cache/storage.py +282 -0
- empathy_os/cli.py +118 -8
- empathy_os/cli_unified.py +121 -1
- empathy_os/config/__init__.py +63 -0
- empathy_os/config/xml_config.py +239 -0
- empathy_os/config.py +2 -1
- empathy_os/dashboard/__init__.py +15 -0
- empathy_os/dashboard/server.py +743 -0
- empathy_os/memory/__init__.py +195 -0
- empathy_os/memory/claude_memory.py +466 -0
- empathy_os/memory/config.py +224 -0
- empathy_os/memory/control_panel.py +1298 -0
- empathy_os/memory/edges.py +179 -0
- empathy_os/memory/graph.py +567 -0
- empathy_os/memory/long_term.py +1194 -0
- empathy_os/memory/nodes.py +179 -0
- empathy_os/memory/redis_bootstrap.py +540 -0
- empathy_os/memory/security/__init__.py +31 -0
- empathy_os/memory/security/audit_logger.py +930 -0
- empathy_os/memory/security/pii_scrubber.py +640 -0
- empathy_os/memory/security/secrets_detector.py +678 -0
- empathy_os/memory/short_term.py +2119 -0
- empathy_os/memory/storage/__init__.py +15 -0
- empathy_os/memory/summary_index.py +583 -0
- empathy_os/memory/unified.py +619 -0
- empathy_os/metrics/__init__.py +12 -0
- empathy_os/metrics/prompt_metrics.py +190 -0
- empathy_os/models/__init__.py +136 -0
- empathy_os/models/__main__.py +13 -0
- empathy_os/models/cli.py +655 -0
- empathy_os/models/empathy_executor.py +354 -0
- empathy_os/models/executor.py +252 -0
- empathy_os/models/fallback.py +671 -0
- empathy_os/models/provider_config.py +563 -0
- empathy_os/models/registry.py +382 -0
- empathy_os/models/tasks.py +302 -0
- empathy_os/models/telemetry.py +548 -0
- empathy_os/models/token_estimator.py +378 -0
- empathy_os/models/validation.py +274 -0
- empathy_os/monitoring/__init__.py +52 -0
- empathy_os/monitoring/alerts.py +23 -0
- empathy_os/monitoring/alerts_cli.py +268 -0
- empathy_os/monitoring/multi_backend.py +271 -0
- empathy_os/monitoring/otel_backend.py +363 -0
- empathy_os/optimization/__init__.py +19 -0
- empathy_os/optimization/context_optimizer.py +272 -0
- empathy_os/plugins/__init__.py +28 -0
- empathy_os/plugins/base.py +361 -0
- empathy_os/plugins/registry.py +268 -0
- empathy_os/project_index/__init__.py +30 -0
- empathy_os/project_index/cli.py +335 -0
- empathy_os/project_index/crew_integration.py +430 -0
- empathy_os/project_index/index.py +425 -0
- empathy_os/project_index/models.py +501 -0
- empathy_os/project_index/reports.py +473 -0
- empathy_os/project_index/scanner.py +538 -0
- empathy_os/prompts/__init__.py +61 -0
- empathy_os/prompts/config.py +77 -0
- empathy_os/prompts/context.py +177 -0
- empathy_os/prompts/parser.py +285 -0
- empathy_os/prompts/registry.py +313 -0
- empathy_os/prompts/templates.py +208 -0
- empathy_os/resilience/__init__.py +56 -0
- empathy_os/resilience/circuit_breaker.py +256 -0
- empathy_os/resilience/fallback.py +179 -0
- empathy_os/resilience/health.py +300 -0
- empathy_os/resilience/retry.py +209 -0
- empathy_os/resilience/timeout.py +135 -0
- empathy_os/routing/__init__.py +43 -0
- empathy_os/routing/chain_executor.py +433 -0
- empathy_os/routing/classifier.py +217 -0
- empathy_os/routing/smart_router.py +234 -0
- empathy_os/routing/wizard_registry.py +307 -0
- empathy_os/trust/__init__.py +28 -0
- empathy_os/trust/circuit_breaker.py +579 -0
- empathy_os/validation/__init__.py +19 -0
- empathy_os/validation/xml_validator.py +281 -0
- empathy_os/wizard_factory_cli.py +170 -0
- empathy_os/workflows/__init__.py +360 -0
- empathy_os/workflows/base.py +1660 -0
- empathy_os/workflows/bug_predict.py +962 -0
- empathy_os/workflows/code_review.py +960 -0
- empathy_os/workflows/code_review_adapters.py +310 -0
- empathy_os/workflows/code_review_pipeline.py +720 -0
- empathy_os/workflows/config.py +600 -0
- empathy_os/workflows/dependency_check.py +648 -0
- empathy_os/workflows/document_gen.py +1069 -0
- empathy_os/workflows/documentation_orchestrator.py +1205 -0
- empathy_os/workflows/health_check.py +679 -0
- empathy_os/workflows/keyboard_shortcuts/__init__.py +39 -0
- empathy_os/workflows/keyboard_shortcuts/generators.py +386 -0
- empathy_os/workflows/keyboard_shortcuts/parsers.py +414 -0
- empathy_os/workflows/keyboard_shortcuts/prompts.py +295 -0
- empathy_os/workflows/keyboard_shortcuts/schema.py +193 -0
- empathy_os/workflows/keyboard_shortcuts/workflow.py +505 -0
- empathy_os/workflows/manage_documentation.py +804 -0
- empathy_os/workflows/new_sample_workflow1.py +146 -0
- empathy_os/workflows/new_sample_workflow1_README.md +150 -0
- empathy_os/workflows/perf_audit.py +687 -0
- empathy_os/workflows/pr_review.py +748 -0
- empathy_os/workflows/progress.py +445 -0
- empathy_os/workflows/progress_server.py +322 -0
- empathy_os/workflows/refactor_plan.py +693 -0
- empathy_os/workflows/release_prep.py +808 -0
- empathy_os/workflows/research_synthesis.py +404 -0
- empathy_os/workflows/secure_release.py +585 -0
- empathy_os/workflows/security_adapters.py +297 -0
- empathy_os/workflows/security_audit.py +1046 -0
- empathy_os/workflows/step_config.py +234 -0
- empathy_os/workflows/test5.py +125 -0
- empathy_os/workflows/test5_README.md +158 -0
- empathy_os/workflows/test_gen.py +1855 -0
- empathy_os/workflows/test_lifecycle.py +526 -0
- empathy_os/workflows/test_maintenance.py +626 -0
- empathy_os/workflows/test_maintenance_cli.py +590 -0
- empathy_os/workflows/test_maintenance_crew.py +821 -0
- empathy_os/workflows/xml_enhanced_crew.py +285 -0
- empathy_software_plugin/cli/__init__.py +120 -0
- empathy_software_plugin/cli/inspect.py +362 -0
- empathy_software_plugin/cli.py +3 -1
- empathy_software_plugin/wizards/__init__.py +42 -0
- empathy_software_plugin/wizards/advanced_debugging_wizard.py +392 -0
- empathy_software_plugin/wizards/agent_orchestration_wizard.py +511 -0
- empathy_software_plugin/wizards/ai_collaboration_wizard.py +503 -0
- empathy_software_plugin/wizards/ai_context_wizard.py +441 -0
- empathy_software_plugin/wizards/ai_documentation_wizard.py +503 -0
- empathy_software_plugin/wizards/base_wizard.py +288 -0
- empathy_software_plugin/wizards/book_chapter_wizard.py +519 -0
- empathy_software_plugin/wizards/code_review_wizard.py +606 -0
- empathy_software_plugin/wizards/debugging/__init__.py +50 -0
- empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +414 -0
- empathy_software_plugin/wizards/debugging/config_loaders.py +442 -0
- empathy_software_plugin/wizards/debugging/fix_applier.py +469 -0
- empathy_software_plugin/wizards/debugging/language_patterns.py +383 -0
- empathy_software_plugin/wizards/debugging/linter_parsers.py +470 -0
- empathy_software_plugin/wizards/debugging/verification.py +369 -0
- empathy_software_plugin/wizards/enhanced_testing_wizard.py +537 -0
- empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +816 -0
- empathy_software_plugin/wizards/multi_model_wizard.py +501 -0
- empathy_software_plugin/wizards/pattern_extraction_wizard.py +422 -0
- empathy_software_plugin/wizards/pattern_retriever_wizard.py +400 -0
- empathy_software_plugin/wizards/performance/__init__.py +9 -0
- empathy_software_plugin/wizards/performance/bottleneck_detector.py +221 -0
- empathy_software_plugin/wizards/performance/profiler_parsers.py +278 -0
- empathy_software_plugin/wizards/performance/trajectory_analyzer.py +429 -0
- empathy_software_plugin/wizards/performance_profiling_wizard.py +305 -0
- empathy_software_plugin/wizards/prompt_engineering_wizard.py +425 -0
- empathy_software_plugin/wizards/rag_pattern_wizard.py +461 -0
- empathy_software_plugin/wizards/security/__init__.py +32 -0
- empathy_software_plugin/wizards/security/exploit_analyzer.py +290 -0
- empathy_software_plugin/wizards/security/owasp_patterns.py +241 -0
- empathy_software_plugin/wizards/security/vulnerability_scanner.py +604 -0
- empathy_software_plugin/wizards/security_analysis_wizard.py +322 -0
- empathy_software_plugin/wizards/security_learning_wizard.py +740 -0
- empathy_software_plugin/wizards/tech_debt_wizard.py +726 -0
- empathy_software_plugin/wizards/testing/__init__.py +27 -0
- empathy_software_plugin/wizards/testing/coverage_analyzer.py +459 -0
- empathy_software_plugin/wizards/testing/quality_analyzer.py +531 -0
- empathy_software_plugin/wizards/testing/test_suggester.py +533 -0
- empathy_software_plugin/wizards/testing_wizard.py +274 -0
- hot_reload/README.md +473 -0
- hot_reload/__init__.py +62 -0
- hot_reload/config.py +84 -0
- hot_reload/integration.py +228 -0
- hot_reload/reloader.py +298 -0
- hot_reload/watcher.py +179 -0
- hot_reload/websocket.py +176 -0
- scaffolding/README.md +589 -0
- scaffolding/__init__.py +35 -0
- scaffolding/__main__.py +14 -0
- scaffolding/cli.py +240 -0
- test_generator/__init__.py +38 -0
- test_generator/__main__.py +14 -0
- test_generator/cli.py +226 -0
- test_generator/generator.py +325 -0
- test_generator/risk_analyzer.py +216 -0
- workflow_patterns/__init__.py +33 -0
- workflow_patterns/behavior.py +249 -0
- workflow_patterns/core.py +76 -0
- workflow_patterns/output.py +99 -0
- workflow_patterns/registry.py +255 -0
- workflow_patterns/structural.py +288 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
- agents/code_inspection/patterns/inspection/recurring_B112.json +0 -18
- agents/code_inspection/patterns/inspection/recurring_F541.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_FORMAT.json +0 -25
- agents/code_inspection/patterns/inspection/recurring_bug_20250822_def456.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20250915_abc123.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_3c5b9951.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_97c0f72f.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_a0871d53.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_20251212_a9b6ec41.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_bug_null_001.json +0 -16
- agents/code_inspection/patterns/inspection/recurring_builtin.json +0 -16
- agents/compliance_anticipation_agent.py +0 -1422
- agents/compliance_db.py +0 -339
- agents/epic_integration_wizard.py +0 -530
- agents/notifications.py +0 -291
- agents/trust_building_behaviors.py +0 -872
- empathy_framework-3.7.0.dist-info/RECORD +0 -105
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/WHEEL +0 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/licenses/LICENSE +0 -0
- /empathy_os/{monitoring.py → agent_monitoring.py} +0 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""Project Scanner - Scans codebase to build file index.
|
|
2
|
+
|
|
3
|
+
Analyzes source files, matches them to tests, calculates metrics.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
6
|
+
Licensed under Fair Source 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import fnmatch
|
|
11
|
+
import os
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .models import FileCategory, FileRecord, IndexConfig, ProjectSummary, TestRequirement
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProjectScanner:
|
|
20
|
+
"""Scans a project directory and builds file metadata.
|
|
21
|
+
|
|
22
|
+
Used by ProjectIndex to populate and update the index.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, project_root: str, config: IndexConfig | None = None):
|
|
26
|
+
self.project_root = Path(project_root)
|
|
27
|
+
self.config = config or IndexConfig()
|
|
28
|
+
self._test_file_map: dict[str, str] = {} # source -> test mapping
|
|
29
|
+
|
|
30
|
+
def scan(self) -> tuple[list[FileRecord], ProjectSummary]:
|
|
31
|
+
"""Scan the entire project and return file records and summary.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Tuple of (list of FileRecords, ProjectSummary)
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
records: list[FileRecord] = []
|
|
38
|
+
|
|
39
|
+
# First pass: discover all files
|
|
40
|
+
all_files = self._discover_files()
|
|
41
|
+
|
|
42
|
+
# Build test file mapping
|
|
43
|
+
self._build_test_mapping(all_files)
|
|
44
|
+
|
|
45
|
+
# Second pass: analyze each file
|
|
46
|
+
for file_path in all_files:
|
|
47
|
+
record = self._analyze_file(file_path)
|
|
48
|
+
if record:
|
|
49
|
+
records.append(record)
|
|
50
|
+
|
|
51
|
+
# Third pass: build dependency graph
|
|
52
|
+
self._analyze_dependencies(records)
|
|
53
|
+
|
|
54
|
+
# Calculate impact scores
|
|
55
|
+
self._calculate_impact_scores(records)
|
|
56
|
+
|
|
57
|
+
# Determine attention needs
|
|
58
|
+
self._determine_attention_needs(records)
|
|
59
|
+
|
|
60
|
+
# Build summary
|
|
61
|
+
summary = self._build_summary(records)
|
|
62
|
+
|
|
63
|
+
return records, summary
|
|
64
|
+
|
|
65
|
+
def _discover_files(self) -> list[Path]:
|
|
66
|
+
"""Discover all relevant files in the project."""
|
|
67
|
+
files = []
|
|
68
|
+
|
|
69
|
+
for root, dirs, filenames in os.walk(self.project_root):
|
|
70
|
+
# Filter out excluded directories
|
|
71
|
+
dirs[:] = [d for d in dirs if not self._is_excluded(Path(root) / d)]
|
|
72
|
+
|
|
73
|
+
for filename in filenames:
|
|
74
|
+
file_path = Path(root) / filename
|
|
75
|
+
rel_path = file_path.relative_to(self.project_root)
|
|
76
|
+
|
|
77
|
+
if not self._is_excluded(rel_path):
|
|
78
|
+
files.append(file_path)
|
|
79
|
+
|
|
80
|
+
return files
|
|
81
|
+
|
|
82
|
+
def _matches_glob_pattern(self, path: Path, pattern: str) -> bool:
|
|
83
|
+
"""Check if a path matches a glob pattern (handles ** patterns)."""
|
|
84
|
+
rel_str = str(path)
|
|
85
|
+
path_parts = path.parts
|
|
86
|
+
|
|
87
|
+
# Handle ** glob patterns
|
|
88
|
+
if "**" in pattern:
|
|
89
|
+
# Convert ** pattern to work with fnmatch
|
|
90
|
+
# **/ at start means any path prefix
|
|
91
|
+
simple_pattern = pattern.replace("**/", "")
|
|
92
|
+
|
|
93
|
+
# Check if the pattern matches the path or any part of it
|
|
94
|
+
if fnmatch.fnmatch(rel_str, simple_pattern):
|
|
95
|
+
return True
|
|
96
|
+
if fnmatch.fnmatch(path.name, simple_pattern):
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
# Check directory-based exclusions
|
|
100
|
+
if pattern.endswith("/**"):
|
|
101
|
+
dir_name = pattern.replace("**/", "").replace("/**", "")
|
|
102
|
+
if dir_name in path_parts:
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
# Check for directory patterns like **/node_modules/**
|
|
106
|
+
if pattern.startswith("**/") and pattern.endswith("/**"):
|
|
107
|
+
dir_name = pattern[3:-3] # Extract directory name
|
|
108
|
+
if dir_name in path_parts:
|
|
109
|
+
return True
|
|
110
|
+
else:
|
|
111
|
+
if fnmatch.fnmatch(rel_str, pattern):
|
|
112
|
+
return True
|
|
113
|
+
if fnmatch.fnmatch(path.name, pattern):
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
def _is_excluded(self, path: Path) -> bool:
|
|
119
|
+
"""Check if a path should be excluded."""
|
|
120
|
+
for pattern in self.config.exclude_patterns:
|
|
121
|
+
if self._matches_glob_pattern(path, pattern):
|
|
122
|
+
return True
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
def _build_test_mapping(self, files: list[Path]) -> None:
|
|
126
|
+
"""Build mapping from source files to their test files."""
|
|
127
|
+
test_files = [f for f in files if self._is_test_file(f)]
|
|
128
|
+
|
|
129
|
+
for test_file in test_files:
|
|
130
|
+
# Try to find corresponding source file
|
|
131
|
+
test_name = test_file.stem # e.g., "test_core"
|
|
132
|
+
|
|
133
|
+
# Common patterns: test_foo.py -> foo.py
|
|
134
|
+
if test_name.startswith("test_"):
|
|
135
|
+
source_name = test_name[5:] # Remove "test_" prefix
|
|
136
|
+
elif test_name.endswith("_test"):
|
|
137
|
+
source_name = test_name[:-5] # Remove "_test" suffix
|
|
138
|
+
else:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Search for matching source file
|
|
142
|
+
for source_file in files:
|
|
143
|
+
if source_file.stem == source_name and not self._is_test_file(source_file):
|
|
144
|
+
rel_source = str(source_file.relative_to(self.project_root))
|
|
145
|
+
rel_test = str(test_file.relative_to(self.project_root))
|
|
146
|
+
self._test_file_map[rel_source] = rel_test
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
def _is_test_file(self, path: Path) -> bool:
|
|
150
|
+
"""Check if a file is a test file."""
|
|
151
|
+
name = path.stem
|
|
152
|
+
return (
|
|
153
|
+
name.startswith("test_")
|
|
154
|
+
or name.endswith("_test")
|
|
155
|
+
or "tests" in path.parts
|
|
156
|
+
or path.parent.name == "test"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _analyze_file(self, file_path: Path) -> FileRecord | None:
|
|
160
|
+
"""Analyze a single file and create its record."""
|
|
161
|
+
rel_path = str(file_path.relative_to(self.project_root))
|
|
162
|
+
|
|
163
|
+
# Determine category
|
|
164
|
+
category = self._determine_category(file_path)
|
|
165
|
+
|
|
166
|
+
# Determine language
|
|
167
|
+
language = self._determine_language(file_path)
|
|
168
|
+
|
|
169
|
+
# Get file stats
|
|
170
|
+
try:
|
|
171
|
+
stat = file_path.stat()
|
|
172
|
+
last_modified = datetime.fromtimestamp(stat.st_mtime)
|
|
173
|
+
except OSError:
|
|
174
|
+
last_modified = None
|
|
175
|
+
|
|
176
|
+
# Determine test requirement
|
|
177
|
+
test_requirement = self._determine_test_requirement(file_path, category)
|
|
178
|
+
|
|
179
|
+
# Find associated test file
|
|
180
|
+
test_file_path = self._test_file_map.get(rel_path)
|
|
181
|
+
tests_exist = test_file_path is not None
|
|
182
|
+
|
|
183
|
+
# Get test file modification time
|
|
184
|
+
tests_last_modified = None
|
|
185
|
+
if test_file_path:
|
|
186
|
+
test_full_path = self.project_root / test_file_path
|
|
187
|
+
if test_full_path.exists():
|
|
188
|
+
try:
|
|
189
|
+
tests_last_modified = datetime.fromtimestamp(test_full_path.stat().st_mtime)
|
|
190
|
+
except OSError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
# Calculate staleness
|
|
194
|
+
staleness_days = 0
|
|
195
|
+
is_stale = False
|
|
196
|
+
if last_modified and tests_last_modified:
|
|
197
|
+
if last_modified > tests_last_modified:
|
|
198
|
+
staleness_days = (last_modified - tests_last_modified).days
|
|
199
|
+
is_stale = staleness_days >= self.config.staleness_threshold_days
|
|
200
|
+
|
|
201
|
+
# Analyze code metrics
|
|
202
|
+
metrics = self._analyze_code_metrics(file_path, language)
|
|
203
|
+
|
|
204
|
+
return FileRecord(
|
|
205
|
+
path=rel_path,
|
|
206
|
+
name=file_path.name,
|
|
207
|
+
category=category,
|
|
208
|
+
language=language,
|
|
209
|
+
test_requirement=test_requirement,
|
|
210
|
+
test_file_path=test_file_path,
|
|
211
|
+
tests_exist=tests_exist,
|
|
212
|
+
test_count=metrics.get("test_count", 0),
|
|
213
|
+
coverage_percent=0.0, # Will be populated from coverage data
|
|
214
|
+
last_modified=last_modified,
|
|
215
|
+
tests_last_modified=tests_last_modified,
|
|
216
|
+
last_indexed=datetime.now(),
|
|
217
|
+
staleness_days=staleness_days,
|
|
218
|
+
is_stale=is_stale,
|
|
219
|
+
lines_of_code=metrics.get("lines_of_code", 0),
|
|
220
|
+
lines_of_test=metrics.get("lines_of_test", 0),
|
|
221
|
+
complexity_score=metrics.get("complexity", 0.0),
|
|
222
|
+
has_docstrings=metrics.get("has_docstrings", False),
|
|
223
|
+
has_type_hints=metrics.get("has_type_hints", False),
|
|
224
|
+
lint_issues=0, # Will be populated from linter
|
|
225
|
+
imports=metrics.get("imports", []),
|
|
226
|
+
imported_by=[], # Populated in dependency analysis
|
|
227
|
+
import_count=len(metrics.get("imports", [])),
|
|
228
|
+
imported_by_count=0,
|
|
229
|
+
impact_score=0.0, # Calculated later
|
|
230
|
+
metadata={},
|
|
231
|
+
needs_attention=False,
|
|
232
|
+
attention_reasons=[],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def _determine_category(self, path: Path) -> FileCategory:
|
|
236
|
+
"""Determine the category of a file."""
|
|
237
|
+
if self._is_test_file(path):
|
|
238
|
+
return FileCategory.TEST
|
|
239
|
+
|
|
240
|
+
suffix = path.suffix.lower()
|
|
241
|
+
|
|
242
|
+
# Config files
|
|
243
|
+
if suffix in [".yml", ".yaml", ".toml", ".ini", ".cfg", ".json"]:
|
|
244
|
+
return FileCategory.CONFIG
|
|
245
|
+
|
|
246
|
+
# Documentation
|
|
247
|
+
if suffix in [".md", ".rst", ".txt"] or path.name in ["README", "CHANGELOG", "LICENSE"]:
|
|
248
|
+
return FileCategory.DOCS
|
|
249
|
+
|
|
250
|
+
# Assets
|
|
251
|
+
if suffix in [".css", ".scss", ".html", ".svg", ".png", ".jpg", ".gif"]:
|
|
252
|
+
return FileCategory.ASSET
|
|
253
|
+
|
|
254
|
+
# Source code
|
|
255
|
+
if suffix in [".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java"]:
|
|
256
|
+
return FileCategory.SOURCE
|
|
257
|
+
|
|
258
|
+
return FileCategory.UNKNOWN
|
|
259
|
+
|
|
260
|
+
def _determine_language(self, path: Path) -> str:
|
|
261
|
+
"""Determine the programming language of a file."""
|
|
262
|
+
suffix_map = {
|
|
263
|
+
".py": "python",
|
|
264
|
+
".js": "javascript",
|
|
265
|
+
".ts": "typescript",
|
|
266
|
+
".tsx": "typescript",
|
|
267
|
+
".jsx": "javascript",
|
|
268
|
+
".go": "go",
|
|
269
|
+
".rs": "rust",
|
|
270
|
+
".java": "java",
|
|
271
|
+
".rb": "ruby",
|
|
272
|
+
".php": "php",
|
|
273
|
+
".cs": "csharp",
|
|
274
|
+
".cpp": "cpp",
|
|
275
|
+
".c": "c",
|
|
276
|
+
".h": "c",
|
|
277
|
+
".hpp": "cpp",
|
|
278
|
+
}
|
|
279
|
+
return suffix_map.get(path.suffix.lower(), "")
|
|
280
|
+
|
|
281
|
+
def _determine_test_requirement(self, path: Path, category: FileCategory) -> TestRequirement:
|
|
282
|
+
"""Determine if a file requires tests."""
|
|
283
|
+
rel_path = path.relative_to(self.project_root)
|
|
284
|
+
|
|
285
|
+
# Test files don't need tests
|
|
286
|
+
if category == FileCategory.TEST:
|
|
287
|
+
return TestRequirement.NOT_APPLICABLE
|
|
288
|
+
|
|
289
|
+
# Config, docs, assets don't need tests
|
|
290
|
+
if category in [FileCategory.CONFIG, FileCategory.DOCS, FileCategory.ASSET]:
|
|
291
|
+
return TestRequirement.NOT_APPLICABLE
|
|
292
|
+
|
|
293
|
+
# Check exclusion patterns using glob matching
|
|
294
|
+
for pattern in self.config.no_test_patterns:
|
|
295
|
+
if self._matches_glob_pattern(rel_path, pattern):
|
|
296
|
+
return TestRequirement.NOT_APPLICABLE
|
|
297
|
+
|
|
298
|
+
# __init__.py files usually don't need tests unless they have logic
|
|
299
|
+
if path.name == "__init__.py":
|
|
300
|
+
try:
|
|
301
|
+
content = path.read_text(encoding="utf-8", errors="ignore")
|
|
302
|
+
# If it's just imports/exports, no tests needed
|
|
303
|
+
if len(content.strip().split("\n")) < 20:
|
|
304
|
+
return TestRequirement.OPTIONAL
|
|
305
|
+
except OSError:
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
return TestRequirement.REQUIRED
|
|
309
|
+
|
|
310
|
+
def _analyze_code_metrics(self, path: Path, language: str) -> dict[str, Any]:
|
|
311
|
+
"""Analyze code metrics for a file."""
|
|
312
|
+
metrics: dict[str, Any] = {
|
|
313
|
+
"lines_of_code": 0,
|
|
314
|
+
"lines_of_test": 0,
|
|
315
|
+
"complexity": 0.0,
|
|
316
|
+
"has_docstrings": False,
|
|
317
|
+
"has_type_hints": False,
|
|
318
|
+
"imports": [],
|
|
319
|
+
"test_count": 0,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if language != "python":
|
|
323
|
+
# For now, just count lines for non-Python
|
|
324
|
+
try:
|
|
325
|
+
content = path.read_text(encoding="utf-8", errors="ignore")
|
|
326
|
+
metrics["lines_of_code"] = len(content.split("\n"))
|
|
327
|
+
except OSError:
|
|
328
|
+
pass
|
|
329
|
+
return metrics
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
content = path.read_text(encoding="utf-8", errors="ignore")
|
|
333
|
+
lines = content.split("\n")
|
|
334
|
+
metrics["lines_of_code"] = len(
|
|
335
|
+
[line for line in lines if line.strip() and not line.strip().startswith("#")],
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Parse AST for Python files
|
|
339
|
+
try:
|
|
340
|
+
tree = ast.parse(content)
|
|
341
|
+
metrics.update(self._analyze_python_ast(tree))
|
|
342
|
+
except (SyntaxError, ValueError):
|
|
343
|
+
# SyntaxError: invalid Python syntax
|
|
344
|
+
# ValueError: null bytes in source code
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
except OSError:
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
return metrics
|
|
351
|
+
|
|
352
|
+
def _analyze_python_ast(self, tree: ast.AST) -> dict[str, Any]:
|
|
353
|
+
"""Analyze Python AST for metrics."""
|
|
354
|
+
result: dict[str, Any] = {
|
|
355
|
+
"has_docstrings": False,
|
|
356
|
+
"has_type_hints": False,
|
|
357
|
+
"imports": [],
|
|
358
|
+
"test_count": 0,
|
|
359
|
+
"complexity": 0.0,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for node in ast.walk(tree):
|
|
363
|
+
# Check for docstrings
|
|
364
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Module):
|
|
365
|
+
if ast.get_docstring(node):
|
|
366
|
+
result["has_docstrings"] = True
|
|
367
|
+
|
|
368
|
+
# Check for type hints
|
|
369
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
370
|
+
if node.returns or any(arg.annotation for arg in node.args.args):
|
|
371
|
+
result["has_type_hints"] = True
|
|
372
|
+
|
|
373
|
+
# Count test functions
|
|
374
|
+
if node.name.startswith("test_"):
|
|
375
|
+
result["test_count"] += 1
|
|
376
|
+
|
|
377
|
+
# Simple complexity: count branches
|
|
378
|
+
for child in ast.walk(node):
|
|
379
|
+
if isinstance(
|
|
380
|
+
child,
|
|
381
|
+
ast.If | ast.For | ast.While | ast.Try | ast.ExceptHandler,
|
|
382
|
+
):
|
|
383
|
+
result["complexity"] += 1.0
|
|
384
|
+
|
|
385
|
+
# Track imports
|
|
386
|
+
if isinstance(node, ast.Import):
|
|
387
|
+
for alias in node.names:
|
|
388
|
+
result["imports"].append(alias.name)
|
|
389
|
+
elif isinstance(node, ast.ImportFrom):
|
|
390
|
+
if node.module:
|
|
391
|
+
result["imports"].append(node.module)
|
|
392
|
+
|
|
393
|
+
return result
|
|
394
|
+
|
|
395
|
+
def _analyze_dependencies(self, records: list[FileRecord]) -> None:
|
|
396
|
+
"""Build dependency graph between files."""
|
|
397
|
+
# Create lookup by module name
|
|
398
|
+
module_to_path: dict[str, str] = {}
|
|
399
|
+
for record in records:
|
|
400
|
+
if record.language == "python":
|
|
401
|
+
# Convert path to module name
|
|
402
|
+
module_name = record.path.replace("/", ".").replace("\\", ".").rstrip(".py")
|
|
403
|
+
module_to_path[module_name] = record.path
|
|
404
|
+
|
|
405
|
+
# Update imported_by relationships
|
|
406
|
+
for record in records:
|
|
407
|
+
for imp in record.imports:
|
|
408
|
+
# Find the imported module
|
|
409
|
+
for module_name, path in module_to_path.items():
|
|
410
|
+
if module_name.endswith(imp) or imp in module_name:
|
|
411
|
+
# Find the record for this path
|
|
412
|
+
for other in records:
|
|
413
|
+
if other.path == path:
|
|
414
|
+
if record.path not in other.imported_by:
|
|
415
|
+
other.imported_by.append(record.path)
|
|
416
|
+
other.imported_by_count = len(other.imported_by)
|
|
417
|
+
break
|
|
418
|
+
break
|
|
419
|
+
|
|
420
|
+
def _calculate_impact_scores(self, records: list[FileRecord]) -> None:
|
|
421
|
+
"""Calculate impact score for each file."""
|
|
422
|
+
for record in records:
|
|
423
|
+
# Impact = imported_by_count * 2 + complexity * 0.5 + lines_of_code * 0.01
|
|
424
|
+
record.impact_score = (
|
|
425
|
+
record.imported_by_count * 2.0
|
|
426
|
+
+ record.complexity_score * 0.5
|
|
427
|
+
+ record.lines_of_code * 0.01
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
def _determine_attention_needs(self, records: list[FileRecord]) -> None:
|
|
431
|
+
"""Determine which files need attention."""
|
|
432
|
+
for record in records:
|
|
433
|
+
reasons = []
|
|
434
|
+
|
|
435
|
+
# Stale tests
|
|
436
|
+
if record.is_stale:
|
|
437
|
+
reasons.append(f"Tests are {record.staleness_days} days stale")
|
|
438
|
+
|
|
439
|
+
# No tests but required
|
|
440
|
+
if record.test_requirement == TestRequirement.REQUIRED and not record.tests_exist:
|
|
441
|
+
reasons.append("Missing tests")
|
|
442
|
+
|
|
443
|
+
# Low coverage (if we have coverage data)
|
|
444
|
+
if (
|
|
445
|
+
record.coverage_percent > 0
|
|
446
|
+
and record.coverage_percent < self.config.low_coverage_threshold
|
|
447
|
+
):
|
|
448
|
+
reasons.append(f"Low coverage ({record.coverage_percent:.1f}%)")
|
|
449
|
+
|
|
450
|
+
# High impact but no tests
|
|
451
|
+
if record.impact_score >= self.config.high_impact_threshold:
|
|
452
|
+
if not record.tests_exist and record.test_requirement == TestRequirement.REQUIRED:
|
|
453
|
+
reasons.append(f"High impact ({record.impact_score:.1f}) without tests")
|
|
454
|
+
|
|
455
|
+
record.attention_reasons = reasons
|
|
456
|
+
record.needs_attention = len(reasons) > 0
|
|
457
|
+
|
|
458
|
+
def _build_summary(self, records: list[FileRecord]) -> ProjectSummary:
|
|
459
|
+
"""Build project summary from records."""
|
|
460
|
+
summary = ProjectSummary()
|
|
461
|
+
|
|
462
|
+
summary.total_files = len(records)
|
|
463
|
+
summary.source_files = sum(1 for r in records if r.category == FileCategory.SOURCE)
|
|
464
|
+
summary.test_files = sum(1 for r in records if r.category == FileCategory.TEST)
|
|
465
|
+
summary.config_files = sum(1 for r in records if r.category == FileCategory.CONFIG)
|
|
466
|
+
summary.doc_files = sum(1 for r in records if r.category == FileCategory.DOCS)
|
|
467
|
+
|
|
468
|
+
# Testing health
|
|
469
|
+
requiring_tests = [r for r in records if r.test_requirement == TestRequirement.REQUIRED]
|
|
470
|
+
summary.files_requiring_tests = len(requiring_tests)
|
|
471
|
+
summary.files_with_tests = sum(1 for r in requiring_tests if r.tests_exist)
|
|
472
|
+
summary.files_without_tests = summary.files_requiring_tests - summary.files_with_tests
|
|
473
|
+
summary.total_test_count = sum(
|
|
474
|
+
r.test_count for r in records if r.category == FileCategory.TEST
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Coverage average
|
|
478
|
+
covered = [r for r in records if r.coverage_percent > 0]
|
|
479
|
+
if covered:
|
|
480
|
+
summary.test_coverage_avg = sum(r.coverage_percent for r in covered) / len(covered)
|
|
481
|
+
|
|
482
|
+
# Staleness
|
|
483
|
+
stale = [r for r in records if r.is_stale]
|
|
484
|
+
summary.stale_file_count = len(stale)
|
|
485
|
+
if stale:
|
|
486
|
+
summary.avg_staleness_days = sum(r.staleness_days for r in stale) / len(stale)
|
|
487
|
+
top_stale = sorted(stale, key=lambda r: -r.staleness_days)[:5]
|
|
488
|
+
summary.most_stale_files = [r.path for r in top_stale]
|
|
489
|
+
|
|
490
|
+
# Code metrics
|
|
491
|
+
source_records = [r for r in records if r.category == FileCategory.SOURCE]
|
|
492
|
+
summary.total_lines_of_code = sum(r.lines_of_code for r in source_records)
|
|
493
|
+
summary.total_lines_of_test = sum(
|
|
494
|
+
r.lines_of_code for r in records if r.category == FileCategory.TEST
|
|
495
|
+
)
|
|
496
|
+
if summary.total_lines_of_code > 0:
|
|
497
|
+
summary.test_to_code_ratio = summary.total_lines_of_test / summary.total_lines_of_code
|
|
498
|
+
if source_records:
|
|
499
|
+
summary.avg_complexity = sum(r.complexity_score for r in source_records) / len(
|
|
500
|
+
source_records,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Quality
|
|
504
|
+
if source_records:
|
|
505
|
+
summary.files_with_docstrings_pct = (
|
|
506
|
+
sum(1 for r in source_records if r.has_docstrings) / len(source_records) * 100
|
|
507
|
+
)
|
|
508
|
+
summary.files_with_type_hints_pct = (
|
|
509
|
+
sum(1 for r in source_records if r.has_type_hints) / len(source_records) * 100
|
|
510
|
+
)
|
|
511
|
+
summary.total_lint_issues = sum(r.lint_issues for r in records)
|
|
512
|
+
|
|
513
|
+
# High impact files
|
|
514
|
+
high_impact = sorted(records, key=lambda r: -r.impact_score)[:10]
|
|
515
|
+
summary.high_impact_files = [
|
|
516
|
+
r.path for r in high_impact if r.impact_score >= self.config.high_impact_threshold
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
# Critical untested files (high impact + no tests)
|
|
520
|
+
critical = [
|
|
521
|
+
r
|
|
522
|
+
for r in records
|
|
523
|
+
if r.impact_score >= self.config.high_impact_threshold
|
|
524
|
+
and not r.tests_exist
|
|
525
|
+
and r.test_requirement == TestRequirement.REQUIRED
|
|
526
|
+
]
|
|
527
|
+
summary.critical_untested_files = [
|
|
528
|
+
r.path for r in sorted(critical, key=lambda r: -r.impact_score)[:10]
|
|
529
|
+
]
|
|
530
|
+
|
|
531
|
+
# Attention needed
|
|
532
|
+
needing_attention = [r for r in records if r.needs_attention]
|
|
533
|
+
summary.files_needing_attention = len(needing_attention)
|
|
534
|
+
summary.top_attention_files = [
|
|
535
|
+
r.path for r in sorted(needing_attention, key=lambda r: -r.impact_score)[:10]
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
return summary
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""XML-Enhanced Prompt System for Empathy Framework
|
|
2
|
+
|
|
3
|
+
Provides structured XML-based prompts for consistent LLM interactions
|
|
4
|
+
and response parsing across workflows.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from empathy_os.prompts import (
|
|
8
|
+
XmlPromptConfig,
|
|
9
|
+
PromptContext,
|
|
10
|
+
XmlPromptTemplate,
|
|
11
|
+
XmlResponseParser,
|
|
12
|
+
get_template,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Create a context
|
|
16
|
+
context = PromptContext.for_security_audit(code="...")
|
|
17
|
+
|
|
18
|
+
# Get a built-in template
|
|
19
|
+
template = get_template("security-audit")
|
|
20
|
+
|
|
21
|
+
# Render the prompt
|
|
22
|
+
prompt = template.render(context)
|
|
23
|
+
|
|
24
|
+
# Parse the response
|
|
25
|
+
parser = XmlResponseParser()
|
|
26
|
+
result = parser.parse(llm_response)
|
|
27
|
+
|
|
28
|
+
if result.success:
|
|
29
|
+
print(result.summary)
|
|
30
|
+
for finding in result.findings:
|
|
31
|
+
print(f"{finding.severity}: {finding.title}")
|
|
32
|
+
|
|
33
|
+
Copyright 2025 Smart-AI-Memory
|
|
34
|
+
Licensed under Fair Source License 0.9
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from .config import XmlPromptConfig
|
|
38
|
+
from .context import PromptContext
|
|
39
|
+
from .parser import Finding, ParsedResponse, XmlResponseParser
|
|
40
|
+
from .registry import BUILTIN_TEMPLATES, get_template, list_templates, register_template
|
|
41
|
+
from .templates import PlainTextPromptTemplate, PromptTemplate, XmlPromptTemplate
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Registry
|
|
45
|
+
"BUILTIN_TEMPLATES",
|
|
46
|
+
"Finding",
|
|
47
|
+
"ParsedResponse",
|
|
48
|
+
"PlainTextPromptTemplate",
|
|
49
|
+
# Context
|
|
50
|
+
"PromptContext",
|
|
51
|
+
# Templates
|
|
52
|
+
"PromptTemplate",
|
|
53
|
+
# Config
|
|
54
|
+
"XmlPromptConfig",
|
|
55
|
+
"XmlPromptTemplate",
|
|
56
|
+
# Parser
|
|
57
|
+
"XmlResponseParser",
|
|
58
|
+
"get_template",
|
|
59
|
+
"list_templates",
|
|
60
|
+
"register_template",
|
|
61
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""XML Prompt Configuration
|
|
2
|
+
|
|
3
|
+
Provides configuration dataclass for XML-enhanced prompts.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart-AI-Memory
|
|
6
|
+
Licensed under Fair Source License 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class XmlPromptConfig:
|
|
17
|
+
"""Configuration for XML prompt behavior.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
enabled: Whether XML prompts are enabled for this workflow/stage.
|
|
21
|
+
schema_version: XML schema version (default "1.0").
|
|
22
|
+
enforce_response_xml: If True, instruct model to respond with XML.
|
|
23
|
+
fallback_on_parse_error: If True, return raw text on XML parse failure.
|
|
24
|
+
template_name: Reference to a built-in template from BUILTIN_TEMPLATES.
|
|
25
|
+
custom_template: Inline XML template string (overrides template_name).
|
|
26
|
+
extra: Additional configuration options.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
enabled: bool = False
|
|
31
|
+
schema_version: str = "1.0"
|
|
32
|
+
enforce_response_xml: bool = False
|
|
33
|
+
fallback_on_parse_error: bool = True
|
|
34
|
+
template_name: str | None = None
|
|
35
|
+
custom_template: str | None = None
|
|
36
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
def merge_with(self, other: XmlPromptConfig) -> XmlPromptConfig:
|
|
39
|
+
"""Merge this config with another, with 'other' taking precedence.
|
|
40
|
+
|
|
41
|
+
Useful for combining global defaults with workflow-specific overrides.
|
|
42
|
+
"""
|
|
43
|
+
merged_extra = {**self.extra, **other.extra}
|
|
44
|
+
return XmlPromptConfig(
|
|
45
|
+
enabled=other.enabled if other.enabled else self.enabled,
|
|
46
|
+
schema_version=other.schema_version or self.schema_version,
|
|
47
|
+
enforce_response_xml=other.enforce_response_xml or self.enforce_response_xml,
|
|
48
|
+
fallback_on_parse_error=other.fallback_on_parse_error,
|
|
49
|
+
template_name=other.template_name or self.template_name,
|
|
50
|
+
custom_template=other.custom_template or self.custom_template,
|
|
51
|
+
extra=merged_extra,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, data: dict[str, Any]) -> XmlPromptConfig:
|
|
56
|
+
"""Create XmlPromptConfig from a dictionary."""
|
|
57
|
+
return cls(
|
|
58
|
+
enabled=data.get("enabled", False),
|
|
59
|
+
schema_version=data.get("schema_version", "1.0"),
|
|
60
|
+
enforce_response_xml=data.get("enforce_response_xml", False),
|
|
61
|
+
fallback_on_parse_error=data.get("fallback_on_parse_error", True),
|
|
62
|
+
template_name=data.get("template_name"),
|
|
63
|
+
custom_template=data.get("custom_template"),
|
|
64
|
+
extra=data.get("extra", {}),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> dict[str, Any]:
|
|
68
|
+
"""Convert to dictionary for serialization."""
|
|
69
|
+
return {
|
|
70
|
+
"enabled": self.enabled,
|
|
71
|
+
"schema_version": self.schema_version,
|
|
72
|
+
"enforce_response_xml": self.enforce_response_xml,
|
|
73
|
+
"fallback_on_parse_error": self.fallback_on_parse_error,
|
|
74
|
+
"template_name": self.template_name,
|
|
75
|
+
"custom_template": self.custom_template,
|
|
76
|
+
"extra": self.extra,
|
|
77
|
+
}
|