gitflow-analytics 1.0.3__py3-none-any.whl → 1.3.11__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 (116) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/classification/__init__.py +31 -0
  3. gitflow_analytics/classification/batch_classifier.py +752 -0
  4. gitflow_analytics/classification/classifier.py +464 -0
  5. gitflow_analytics/classification/feature_extractor.py +725 -0
  6. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  7. gitflow_analytics/classification/model.py +455 -0
  8. gitflow_analytics/cli.py +4158 -350
  9. gitflow_analytics/cli_rich.py +198 -48
  10. gitflow_analytics/config/__init__.py +43 -0
  11. gitflow_analytics/config/errors.py +261 -0
  12. gitflow_analytics/config/loader.py +905 -0
  13. gitflow_analytics/config/profiles.py +264 -0
  14. gitflow_analytics/config/repository.py +124 -0
  15. gitflow_analytics/config/schema.py +444 -0
  16. gitflow_analytics/config/validator.py +154 -0
  17. gitflow_analytics/config.py +44 -508
  18. gitflow_analytics/core/analyzer.py +1209 -98
  19. gitflow_analytics/core/cache.py +1337 -29
  20. gitflow_analytics/core/data_fetcher.py +1285 -0
  21. gitflow_analytics/core/identity.py +363 -14
  22. gitflow_analytics/core/metrics_storage.py +526 -0
  23. gitflow_analytics/core/progress.py +372 -0
  24. gitflow_analytics/core/schema_version.py +269 -0
  25. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  26. gitflow_analytics/extractors/story_points.py +8 -1
  27. gitflow_analytics/extractors/tickets.py +749 -11
  28. gitflow_analytics/identity_llm/__init__.py +6 -0
  29. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  30. gitflow_analytics/identity_llm/analyzer.py +464 -0
  31. gitflow_analytics/identity_llm/models.py +76 -0
  32. gitflow_analytics/integrations/github_integration.py +175 -11
  33. gitflow_analytics/integrations/jira_integration.py +461 -24
  34. gitflow_analytics/integrations/orchestrator.py +124 -1
  35. gitflow_analytics/metrics/activity_scoring.py +322 -0
  36. gitflow_analytics/metrics/branch_health.py +470 -0
  37. gitflow_analytics/metrics/dora.py +379 -20
  38. gitflow_analytics/models/database.py +843 -53
  39. gitflow_analytics/pm_framework/__init__.py +115 -0
  40. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  41. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  42. gitflow_analytics/pm_framework/base.py +406 -0
  43. gitflow_analytics/pm_framework/models.py +211 -0
  44. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  45. gitflow_analytics/pm_framework/registry.py +333 -0
  46. gitflow_analytics/qualitative/__init__.py +9 -10
  47. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  48. gitflow_analytics/qualitative/classifiers/__init__.py +3 -3
  49. gitflow_analytics/qualitative/classifiers/change_type.py +518 -244
  50. gitflow_analytics/qualitative/classifiers/domain_classifier.py +272 -165
  51. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +321 -222
  52. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  53. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  54. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  55. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  56. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  57. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  58. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  59. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  60. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  61. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +215 -189
  62. gitflow_analytics/qualitative/core/__init__.py +4 -4
  63. gitflow_analytics/qualitative/core/llm_fallback.py +239 -235
  64. gitflow_analytics/qualitative/core/nlp_engine.py +157 -148
  65. gitflow_analytics/qualitative/core/pattern_cache.py +214 -192
  66. gitflow_analytics/qualitative/core/processor.py +381 -248
  67. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  68. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  69. gitflow_analytics/qualitative/models/__init__.py +7 -7
  70. gitflow_analytics/qualitative/models/schemas.py +155 -121
  71. gitflow_analytics/qualitative/utils/__init__.py +4 -4
  72. gitflow_analytics/qualitative/utils/batch_processor.py +136 -123
  73. gitflow_analytics/qualitative/utils/cost_tracker.py +142 -140
  74. gitflow_analytics/qualitative/utils/metrics.py +172 -158
  75. gitflow_analytics/qualitative/utils/text_processing.py +146 -104
  76. gitflow_analytics/reports/__init__.py +100 -0
  77. gitflow_analytics/reports/analytics_writer.py +539 -14
  78. gitflow_analytics/reports/base.py +648 -0
  79. gitflow_analytics/reports/branch_health_writer.py +322 -0
  80. gitflow_analytics/reports/classification_writer.py +924 -0
  81. gitflow_analytics/reports/cli_integration.py +427 -0
  82. gitflow_analytics/reports/csv_writer.py +1676 -212
  83. gitflow_analytics/reports/data_models.py +504 -0
  84. gitflow_analytics/reports/database_report_generator.py +427 -0
  85. gitflow_analytics/reports/example_usage.py +344 -0
  86. gitflow_analytics/reports/factory.py +499 -0
  87. gitflow_analytics/reports/formatters.py +698 -0
  88. gitflow_analytics/reports/html_generator.py +1116 -0
  89. gitflow_analytics/reports/interfaces.py +489 -0
  90. gitflow_analytics/reports/json_exporter.py +2770 -0
  91. gitflow_analytics/reports/narrative_writer.py +2287 -158
  92. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  93. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  94. gitflow_analytics/training/__init__.py +5 -0
  95. gitflow_analytics/training/model_loader.py +377 -0
  96. gitflow_analytics/training/pipeline.py +550 -0
  97. gitflow_analytics/tui/__init__.py +1 -1
  98. gitflow_analytics/tui/app.py +129 -126
  99. gitflow_analytics/tui/screens/__init__.py +3 -3
  100. gitflow_analytics/tui/screens/analysis_progress_screen.py +188 -179
  101. gitflow_analytics/tui/screens/configuration_screen.py +154 -178
  102. gitflow_analytics/tui/screens/loading_screen.py +100 -110
  103. gitflow_analytics/tui/screens/main_screen.py +89 -72
  104. gitflow_analytics/tui/screens/results_screen.py +305 -281
  105. gitflow_analytics/tui/widgets/__init__.py +2 -2
  106. gitflow_analytics/tui/widgets/data_table.py +67 -69
  107. gitflow_analytics/tui/widgets/export_modal.py +76 -76
  108. gitflow_analytics/tui/widgets/progress_widget.py +41 -46
  109. gitflow_analytics-1.3.11.dist-info/METADATA +1015 -0
  110. gitflow_analytics-1.3.11.dist-info/RECORD +122 -0
  111. gitflow_analytics-1.0.3.dist-info/METADATA +0 -490
  112. gitflow_analytics-1.0.3.dist-info/RECORD +0 -62
  113. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/WHEEL +0 -0
  114. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/entry_points.txt +0 -0
  115. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/licenses/LICENSE +0 -0
  116. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,333 @@
1
+ """Platform registry for managing and instantiating PM platform adapters.
2
+
3
+ This module provides centralized registration and management of platform adapters,
4
+ including instance creation, caching, and capability discovery.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Optional
9
+
10
+ from .base import BasePlatformAdapter
11
+
12
+ # Configure logger for registry
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class PlatformRegistry:
17
+ """Registry for managing platform adapters.
18
+
19
+ WHY: Centralized registry provides a clean way to discover available
20
+ platforms, manage adapter instances, and ensure consistent initialization
21
+ patterns across all adapters.
22
+
23
+ DESIGN DECISION: Use registry pattern rather than direct imports to enable
24
+ dynamic adapter discovery and plugin-style architecture. This allows
25
+ future extensions without modifying core framework code.
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ """Initialize the platform registry.
30
+
31
+ Creates empty registry for adapter classes and instances.
32
+ Built-in adapters should be registered during module initialization.
33
+ """
34
+ self._adapters: dict[str, type[BasePlatformAdapter]] = {}
35
+ self._instances: dict[str, BasePlatformAdapter] = {}
36
+
37
+ logger.info("Platform registry initialized")
38
+
39
+ def register_adapter(
40
+ self, platform_name: str, adapter_class: type[BasePlatformAdapter]
41
+ ) -> None:
42
+ """Register a platform adapter class.
43
+
44
+ WHY: Registration allows the framework to discover and use platform
45
+ adapters without hardcoding imports. This enables a plugin architecture
46
+ where new adapters can be added by simply registering them.
47
+
48
+ Args:
49
+ platform_name: Unique identifier for the platform (e.g., 'jira').
50
+ adapter_class: Class that implements BasePlatformAdapter interface.
51
+
52
+ Raises:
53
+ ValueError: If platform_name is already registered or invalid.
54
+ TypeError: If adapter_class doesn't inherit from BasePlatformAdapter.
55
+ """
56
+ if not platform_name or not isinstance(platform_name, str):
57
+ raise ValueError("Platform name must be a non-empty string")
58
+
59
+ if not issubclass(adapter_class, BasePlatformAdapter):
60
+ raise TypeError(f"Adapter class {adapter_class} must inherit from BasePlatformAdapter")
61
+
62
+ if platform_name in self._adapters:
63
+ logger.warning(f"Overriding existing adapter for platform: {platform_name}")
64
+
65
+ self._adapters[platform_name] = adapter_class
66
+ logger.info(f"Registered adapter for platform: {platform_name}")
67
+
68
+ def unregister_adapter(self, platform_name: str) -> None:
69
+ """Unregister a platform adapter.
70
+
71
+ WHY: Allows dynamic removal of adapters, useful for testing or
72
+ when adapters need to be replaced at runtime.
73
+
74
+ Args:
75
+ platform_name: Platform identifier to unregister.
76
+ """
77
+ if platform_name in self._adapters:
78
+ del self._adapters[platform_name]
79
+ logger.info(f"Unregistered adapter for platform: {platform_name}")
80
+
81
+ # Also remove any existing instance
82
+ if platform_name in self._instances:
83
+ del self._instances[platform_name]
84
+ logger.info(f"Removed cached instance for platform: {platform_name}")
85
+
86
+ def get_available_platforms(self) -> list[str]:
87
+ """Get list of available platform names.
88
+
89
+ WHY: Allows discovery of available platforms for configuration
90
+ validation and user interface generation.
91
+
92
+ Returns:
93
+ List of registered platform identifiers.
94
+ """
95
+ return list(self._adapters.keys())
96
+
97
+ def is_platform_available(self, platform_name: str) -> bool:
98
+ """Check if a platform adapter is available.
99
+
100
+ Args:
101
+ platform_name: Platform identifier to check.
102
+
103
+ Returns:
104
+ True if platform adapter is registered, False otherwise.
105
+ """
106
+ return platform_name in self._adapters
107
+
108
+ def get_platform_capabilities(self, platform_name: str) -> Optional[dict[str, Any]]:
109
+ """Get capabilities for a platform without instantiating it.
110
+
111
+ WHY: Allows capability discovery for configuration validation and
112
+ feature planning without the overhead of authentication and connection.
113
+
114
+ Args:
115
+ platform_name: Platform identifier to check capabilities for.
116
+
117
+ Returns:
118
+ Dictionary of capabilities, or None if platform not found.
119
+ """
120
+ if platform_name not in self._adapters:
121
+ logger.warning(f"Platform not found: {platform_name}")
122
+ return None
123
+
124
+ try:
125
+ # Create temporary instance to get capabilities
126
+ adapter_class = self._adapters[platform_name]
127
+ temp_adapter = adapter_class({}) # Empty config for capability check only
128
+ capabilities = temp_adapter._get_capabilities()
129
+
130
+ return {
131
+ "supports_projects": capabilities.supports_projects,
132
+ "supports_issues": capabilities.supports_issues,
133
+ "supports_sprints": capabilities.supports_sprints,
134
+ "supports_time_tracking": capabilities.supports_time_tracking,
135
+ "supports_story_points": capabilities.supports_story_points,
136
+ "supports_custom_fields": capabilities.supports_custom_fields,
137
+ "supports_issue_linking": capabilities.supports_issue_linking,
138
+ "supports_comments": capabilities.supports_comments,
139
+ "supports_attachments": capabilities.supports_attachments,
140
+ "supports_workflows": capabilities.supports_workflows,
141
+ "supports_bulk_operations": capabilities.supports_bulk_operations,
142
+ "rate_limit_requests_per_hour": capabilities.rate_limit_requests_per_hour,
143
+ "rate_limit_burst_size": capabilities.rate_limit_burst_size,
144
+ "max_results_per_page": capabilities.max_results_per_page,
145
+ "supports_cursor_pagination": capabilities.supports_cursor_pagination,
146
+ }
147
+ except Exception as e:
148
+ logger.error(f"Failed to get capabilities for {platform_name}: {e}")
149
+ return None
150
+
151
+ def create_adapter(self, platform_name: str, config: dict[str, Any]) -> BasePlatformAdapter:
152
+ """Create and configure a platform adapter instance.
153
+
154
+ WHY: Centralized instance creation ensures consistent initialization
155
+ patterns and enables caching for performance. Authentication is
156
+ performed during creation to fail fast on configuration issues.
157
+
158
+ Args:
159
+ platform_name: Platform identifier to create adapter for.
160
+ config: Platform-specific configuration including credentials.
161
+
162
+ Returns:
163
+ Configured and authenticated adapter instance.
164
+
165
+ Raises:
166
+ ValueError: If platform is not registered.
167
+ ConnectionError: If authentication fails.
168
+ Exception: If adapter initialization fails.
169
+ """
170
+ if platform_name not in self._adapters:
171
+ available = ", ".join(self.get_available_platforms())
172
+ raise ValueError(f"Unknown platform: {platform_name}. Available platforms: {available}")
173
+
174
+ logger.info(f"Creating adapter for platform: {platform_name}")
175
+
176
+ try:
177
+ adapter_class = self._adapters[platform_name]
178
+ adapter = adapter_class(config)
179
+
180
+ # Test authentication during creation
181
+ logger.info(f"Authenticating with {platform_name}...")
182
+ if not adapter.authenticate():
183
+ raise ConnectionError(f"Failed to authenticate with {platform_name}")
184
+
185
+ # Test connection to validate configuration
186
+ connection_info = adapter.test_connection()
187
+ if connection_info.get("status") != "connected":
188
+ error_msg = connection_info.get("error", "Unknown connection error")
189
+ raise ConnectionError(f"Connection test failed for {platform_name}: {error_msg}")
190
+
191
+ # Cache the instance for reuse
192
+ self._instances[platform_name] = adapter
193
+ logger.info(f"Successfully created and cached adapter for {platform_name}")
194
+
195
+ return adapter
196
+
197
+ except Exception as e:
198
+ logger.error(f"Failed to create adapter for {platform_name}: {e}")
199
+ # Remove failed instance from cache
200
+ if platform_name in self._instances:
201
+ del self._instances[platform_name]
202
+ raise
203
+
204
+ def get_adapter(self, platform_name: str) -> Optional[BasePlatformAdapter]:
205
+ """Get existing adapter instance from cache.
206
+
207
+ WHY: Reusing adapter instances avoids repeated authentication and
208
+ connection setup, improving performance for multiple operations.
209
+
210
+ Args:
211
+ platform_name: Platform identifier to retrieve adapter for.
212
+
213
+ Returns:
214
+ Cached adapter instance, or None if not found or not cached.
215
+ """
216
+ return self._instances.get(platform_name)
217
+
218
+ def remove_adapter_instance(self, platform_name: str) -> None:
219
+ """Remove adapter instance from cache.
220
+
221
+ WHY: Allows forcing recreation of adapter instances, useful when
222
+ configuration changes or connection issues require fresh authentication.
223
+
224
+ Args:
225
+ platform_name: Platform identifier to remove from cache.
226
+ """
227
+ if platform_name in self._instances:
228
+ del self._instances[platform_name]
229
+ logger.info(f"Removed cached adapter instance for {platform_name}")
230
+
231
+ def clear_all_instances(self) -> None:
232
+ """Clear all cached adapter instances.
233
+
234
+ WHY: Useful for cleanup operations, testing, or when configuration
235
+ changes require fresh adapter creation.
236
+ """
237
+ instance_count = len(self._instances)
238
+ self._instances.clear()
239
+ logger.info(f"Cleared {instance_count} cached adapter instances")
240
+
241
+ def validate_config(self, platform_name: str, config: dict[str, Any]) -> dict[str, Any]:
242
+ """Validate configuration for a platform without creating full adapter.
243
+
244
+ WHY: Allows configuration validation during setup or testing without
245
+ the overhead of full adapter creation and authentication.
246
+
247
+ Args:
248
+ platform_name: Platform identifier to validate config for.
249
+ config: Configuration to validate.
250
+
251
+ Returns:
252
+ Dictionary with validation results including status and any errors.
253
+ """
254
+ if platform_name not in self._adapters:
255
+ return {
256
+ "valid": False,
257
+ "error": f"Unknown platform: {platform_name}",
258
+ "missing_fields": [],
259
+ "invalid_fields": [],
260
+ }
261
+
262
+ try:
263
+ # Get adapter class to check required config fields
264
+ adapter_class = self._adapters[platform_name]
265
+
266
+ # Create instance with config for basic validation
267
+ # Note: This doesn't call authenticate() to avoid side effects
268
+ temp_adapter = adapter_class(config)
269
+
270
+ # Basic validation passed if we got here
271
+ return {
272
+ "valid": True,
273
+ "platform_name": temp_adapter.platform_name,
274
+ "capabilities": self.get_platform_capabilities(platform_name),
275
+ "missing_fields": [],
276
+ "invalid_fields": [],
277
+ }
278
+
279
+ except KeyError as e:
280
+ return {
281
+ "valid": False,
282
+ "error": f"Missing required configuration field: {str(e)}",
283
+ "missing_fields": [str(e)],
284
+ "invalid_fields": [],
285
+ }
286
+ except ValueError as e:
287
+ return {
288
+ "valid": False,
289
+ "error": f"Invalid configuration value: {str(e)}",
290
+ "missing_fields": [],
291
+ "invalid_fields": [str(e)],
292
+ }
293
+ except Exception as e:
294
+ return {
295
+ "valid": False,
296
+ "error": f"Configuration validation failed: {str(e)}",
297
+ "missing_fields": [],
298
+ "invalid_fields": [],
299
+ }
300
+
301
+ def get_registry_status(self) -> dict[str, Any]:
302
+ """Get current registry status and statistics.
303
+
304
+ WHY: Provides diagnostic information for monitoring and debugging
305
+ the registry state, useful for administration and troubleshooting.
306
+
307
+ Returns:
308
+ Dictionary containing registry statistics and status information.
309
+ """
310
+ active_instances = {}
311
+ for platform_name, adapter in self._instances.items():
312
+ try:
313
+ # Test if cached instance is still valid
314
+ connection_info = adapter.test_connection()
315
+ active_instances[platform_name] = {
316
+ "status": connection_info.get("status", "unknown"),
317
+ "platform": adapter.platform_name,
318
+ "capabilities_count": sum(
319
+ 1
320
+ for k, v in vars(adapter.capabilities).items()
321
+ if k.startswith("supports_") and v
322
+ ),
323
+ }
324
+ except Exception as e:
325
+ active_instances[platform_name] = {"status": "error", "error": str(e)}
326
+
327
+ return {
328
+ "registered_platforms": len(self._adapters),
329
+ "available_platforms": self.get_available_platforms(),
330
+ "cached_instances": len(self._instances),
331
+ "active_instances": active_instances,
332
+ "registry_initialized": True,
333
+ }
@@ -6,25 +6,24 @@ file changes.
6
6
 
7
7
  Key Components:
8
8
  - QualitativeProcessor: Main orchestrator for qualitative analysis
9
- - NLPEngine: spaCy-based fast processing for most commits
9
+ - EnhancedQualitativeAnalyzer: Advanced multi-dimensional analysis for executives, projects, developers, and workflows
10
+ - NLPEngine: spaCy-based fast processing for most commits
10
11
  - LLMFallback: Strategic use of LLMs for uncertain cases
11
12
  - Various classifiers for change type, domain, risk, and intent analysis
12
13
  """
13
14
 
14
15
  from .core.processor import QualitativeProcessor
15
- from .models.schemas import (
16
- QualitativeCommitData,
17
- QualitativeConfig,
18
- NLPConfig,
19
- LLMConfig,
20
- CacheConfig as QualitativeCacheConfig,
21
- )
16
+
17
+ # from .enhanced_analyzer import EnhancedQualitativeAnalyzer # Commented out - missing dependencies
18
+ from .models.schemas import CacheConfig as QualitativeCacheConfig
19
+ from .models.schemas import LLMConfig, NLPConfig, QualitativeCommitData, QualitativeConfig
22
20
 
23
21
  __all__ = [
24
22
  "QualitativeProcessor",
25
- "QualitativeCommitData",
23
+ # "EnhancedQualitativeAnalyzer", # Commented out - missing dependencies
24
+ "QualitativeCommitData",
26
25
  "QualitativeConfig",
27
26
  "NLPConfig",
28
27
  "LLMConfig",
29
28
  "QualitativeCacheConfig",
30
- ]
29
+ ]
@@ -0,0 +1,259 @@
1
+ """ChatGPT-based qualitative analysis for GitFlow Analytics.
2
+
3
+ This module uses OpenAI's ChatGPT-4.1 to generate insightful executive summaries
4
+ based on comprehensive GitFlow Analytics data.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ import requests
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ChatGPTQualitativeAnalyzer:
19
+ """Generate qualitative insights using ChatGPT-4.1."""
20
+
21
+ def __init__(self, api_key: Optional[str] = None):
22
+ """Initialize the ChatGPT analyzer.
23
+
24
+ Args:
25
+ api_key: API key. If not provided, uses OPENROUTER_API_KEY or OPENAI_API_KEY env var.
26
+ """
27
+ self.api_key = api_key or os.getenv("OPENROUTER_API_KEY") or os.getenv("OPENAI_API_KEY")
28
+ if not self.api_key:
29
+ raise ValueError(
30
+ "API key not provided. Set OPENROUTER_API_KEY or OPENAI_API_KEY environment variable."
31
+ )
32
+
33
+ # Check if this is an OpenRouter key
34
+ self.use_openrouter = self.api_key.startswith("sk-or-")
35
+ if self.use_openrouter:
36
+ self.api_url = "https://openrouter.ai/api/v1/chat/completions"
37
+ self.model = "openai/gpt-4-turbo-preview" # OpenRouter model name
38
+ else:
39
+ # Fallback to OpenAI direct
40
+ from openai import OpenAI
41
+
42
+ self.client = OpenAI(api_key=self.api_key)
43
+ self.model = "gpt-4-turbo-preview"
44
+
45
+ def generate_executive_summary(self, comprehensive_data: dict[str, Any]) -> str:
46
+ """Generate a qualitative executive summary from comprehensive export data.
47
+
48
+ Args:
49
+ comprehensive_data: The comprehensive JSON export data
50
+
51
+ Returns:
52
+ A markdown-formatted executive summary with qualitative insights
53
+ """
54
+ # Extract key metrics for the prompt
55
+ summary_data = self._extract_summary_data(comprehensive_data)
56
+
57
+ # Create the prompt
58
+ prompt = self._create_executive_summary_prompt(summary_data)
59
+
60
+ try:
61
+ # Call ChatGPT
62
+ messages = [
63
+ {
64
+ "role": "system",
65
+ "content": "You are a data-driven software development analyst providing objective insights on team productivity and project health. Use factual, analytical language. Strictly avoid: promotional language, subjective assessments, marketing terms, superlatives (best, worst, great, poor), praise words (impressive, commendable, excellent, strong, effective), emotional qualifiers (positive, negative, concerning), interpretive language (suggests, indicates potential, appears to show). Report only measurable patterns, quantifiable trends, and evidence-based observations. Use neutral technical terminology. Present findings as statistical facts without subjective interpretation.",
66
+ },
67
+ {"role": "user", "content": prompt},
68
+ ]
69
+
70
+ if self.use_openrouter:
71
+ # Use OpenRouter API
72
+ headers = {
73
+ "Authorization": f"Bearer {self.api_key}",
74
+ "Content-Type": "application/json",
75
+ "HTTP-Referer": "https://github.com/EWTN-Global/gitflow-analytics",
76
+ "X-Title": "GitFlow Analytics",
77
+ }
78
+
79
+ data = {
80
+ "model": self.model,
81
+ "messages": messages,
82
+ "max_tokens": 1500,
83
+ "temperature": 0.7,
84
+ }
85
+
86
+ response = requests.post(self.api_url, headers=headers, json=data)
87
+ response.raise_for_status()
88
+ result = response.json()
89
+ content = result["choices"][0]["message"]["content"]
90
+ else:
91
+ # Use OpenAI directly
92
+ response = self.client.chat.completions.create(
93
+ model=self.model, messages=messages, temperature=0.7, max_tokens=1500
94
+ )
95
+ content = response.choices[0].message.content
96
+
97
+ return content
98
+
99
+ except Exception as e:
100
+ logger.error(f"Error generating ChatGPT summary: {e}")
101
+ return self._generate_fallback_summary(summary_data)
102
+
103
+ def _extract_summary_data(self, data: dict[str, Any]) -> dict[str, Any]:
104
+ """Extract key data points for the executive summary."""
105
+
106
+ exec_summary = data.get("executive_summary", {})
107
+ metadata = data.get("metadata", {})
108
+ projects = data.get("projects", {})
109
+ developers = data.get("developers", {})
110
+
111
+ # Get top contributors
112
+ top_developers = []
113
+ for _dev_id, dev_data in developers.items():
114
+ identity = dev_data.get("identity", {})
115
+ summary = dev_data.get("summary", {})
116
+ top_developers.append(
117
+ {
118
+ "name": identity.get("name", "Unknown"),
119
+ "commits": summary.get("total_commits", 0),
120
+ "story_points": summary.get("total_story_points", 0),
121
+ "projects": len(dev_data.get("projects", {})),
122
+ }
123
+ )
124
+ top_developers.sort(key=lambda x: x["commits"], reverse=True)
125
+
126
+ # Get project health
127
+ project_health = []
128
+ for proj_key, proj_data in projects.items():
129
+ health = proj_data.get("health_score", {})
130
+ summary = proj_data.get("summary", {})
131
+ project_health.append(
132
+ {
133
+ "name": proj_key,
134
+ "health_score": health.get("overall", 0),
135
+ "health_rating": health.get("rating", "unknown"),
136
+ "commits": summary.get("total_commits", 0),
137
+ "contributors": summary.get("total_contributors", 0),
138
+ }
139
+ )
140
+ project_health.sort(key=lambda x: x["commits"], reverse=True)
141
+
142
+ return {
143
+ "period_weeks": metadata.get("analysis_weeks", 0),
144
+ "total_commits": exec_summary.get("key_metrics", {}).get("commits", {}).get("total", 0),
145
+ "total_developers": exec_summary.get("key_metrics", {})
146
+ .get("developers", {})
147
+ .get("total", 0),
148
+ "lines_changed": exec_summary.get("key_metrics", {})
149
+ .get("lines_changed", {})
150
+ .get("total", 0),
151
+ "story_points": exec_summary.get("key_metrics", {})
152
+ .get("story_points", {})
153
+ .get("total", 0),
154
+ "ticket_coverage": exec_summary.get("key_metrics", {})
155
+ .get("ticket_coverage", {})
156
+ .get("percentage", 0),
157
+ "team_health_score": exec_summary.get("health_score", {}).get("overall", 0),
158
+ "team_health_rating": exec_summary.get("health_score", {}).get("rating", "unknown"),
159
+ "top_developers": top_developers[:5],
160
+ "project_health": project_health[:5],
161
+ "velocity_trend": exec_summary.get("trends", {})
162
+ .get("velocity", {})
163
+ .get("direction", "stable"),
164
+ "wins": exec_summary.get("wins", [])[:3],
165
+ "concerns": exec_summary.get("concerns", [])[:3],
166
+ "anomalies": data.get("anomaly_detection", {}).get("anomalies", [])[:3],
167
+ }
168
+
169
+ def _create_executive_summary_prompt(self, summary_data: dict[str, Any]) -> str:
170
+ """Create the prompt for ChatGPT."""
171
+
172
+ prompt = f"""Based on the following GitFlow Analytics data from the past {summary_data['period_weeks']} weeks, provide a comprehensive executive summary with qualitative insights:
173
+
174
+ ## Key Metrics:
175
+ - Total Commits: {summary_data['total_commits']:,}
176
+ - Active Developers: {summary_data['total_developers']}
177
+ - Lines Changed: {summary_data['lines_changed']:,}
178
+ - Story Points Delivered: {summary_data['story_points']}
179
+ - Ticket Coverage: {summary_data['ticket_coverage']:.1f}%
180
+ - Team Health Score: {summary_data['team_health_score']:.1f}/100 ({summary_data['team_health_rating']})
181
+ - Velocity Trend: {summary_data['velocity_trend']}
182
+
183
+ ## Top Contributors:
184
+ """
185
+
186
+ for dev in summary_data["top_developers"]:
187
+ prompt += f"- {dev['name']}: {dev['commits']} commits, {dev['story_points']} story points across {dev['projects']} projects\n"
188
+
189
+ prompt += "\n## Project Health:\n"
190
+ for proj in summary_data["project_health"]:
191
+ prompt += f"- {proj['name']}: Health {proj['health_score']:.1f}/100 ({proj['health_rating']}), {proj['commits']} commits, {proj['contributors']} contributors\n"
192
+
193
+ if summary_data["wins"]:
194
+ prompt += "\n## Recent Wins:\n"
195
+ for win in summary_data["wins"]:
196
+ prompt += f"- {win.get('title', 'Achievement')}: {win.get('description', '')}\n"
197
+
198
+ if summary_data["concerns"]:
199
+ prompt += "\n## Areas of Concern:\n"
200
+ for concern in summary_data["concerns"]:
201
+ prompt += f"- {concern.get('title', 'Issue')}: {concern.get('description', '')}\n"
202
+
203
+ if summary_data["anomalies"]:
204
+ prompt += "\n## Detected Anomalies:\n"
205
+ for anomaly in summary_data["anomalies"]:
206
+ prompt += f"- {anomaly.get('type', 'anomaly')} in {anomaly.get('metric', 'unknown')}: {anomaly.get('description', '')}\n"
207
+
208
+ prompt += """
209
+ Provide a technical analysis with:
210
+ 1. A 2-3 paragraph data summary reporting team output patterns, work distribution, and process metrics
211
+ 2. Quantitative observations about team dynamics derived from the metrics (commit frequency, ticket coverage ratios, health score calculations)
212
+ 3. 3-5 specific, data-driven recommendations with measurable targets based on metric thresholds
213
+ 4. Observable patterns in the data that deviate from baseline measurements (if applicable)
214
+
215
+ Report only statistical patterns, measurable trends, and process gaps. Use factual technical language. Base all statements on quantifiable metrics and their mathematical relationships. Avoid subjective interpretations or assessments.
216
+ """
217
+
218
+ return prompt
219
+
220
+ def _generate_fallback_summary(self, summary_data: dict[str, Any]) -> str:
221
+ """Generate a basic summary if ChatGPT fails."""
222
+
223
+ return f"""## Executive Summary
224
+
225
+ Over the past {summary_data['period_weeks']} weeks, the development team generated {summary_data['total_commits']:,} commits across {summary_data['total_developers']} active developers.
226
+
227
+ The team health score measured {summary_data['team_health_score']:.1f}/100 ({summary_data['team_health_rating']}). Ticket coverage reached {summary_data['ticket_coverage']:.1f}% of total commits with trackable references.
228
+
229
+ ### Measured Outputs:
230
+ - Code changes: {summary_data['lines_changed']:,} lines modified
231
+ - Story points completed: {summary_data['story_points']}
232
+ - Velocity trend: {summary_data['velocity_trend']}
233
+
234
+ ### Process Recommendations:
235
+ 1. {'Maintain current output rate' if summary_data['velocity_trend'] == 'increasing' else 'Analyze velocity decline factors'}
236
+ 2. {'Sustain current tracking rate' if summary_data['ticket_coverage'] > 60 else 'Increase commit-ticket linking to reach 70% coverage target'}
237
+ 3. Review projects with health scores below 60/100 for process gaps
238
+
239
+ *Note: This is a fallback summary. For detailed analysis, configure ChatGPT integration.*
240
+ """
241
+
242
+
243
+ def generate_chatgpt_summary(json_file_path: Path, api_key: Optional[str] = None) -> str:
244
+ """Convenience function to generate a ChatGPT summary from a JSON export file.
245
+
246
+ Args:
247
+ json_file_path: Path to the comprehensive JSON export
248
+ api_key: Optional OpenAI API key
249
+
250
+ Returns:
251
+ Markdown-formatted executive summary
252
+ """
253
+ # Load the JSON data
254
+ with open(json_file_path) as f:
255
+ comprehensive_data = json.load(f)
256
+
257
+ # Generate summary
258
+ analyzer = ChatGPTQualitativeAnalyzer(api_key)
259
+ return analyzer.generate_executive_summary(comprehensive_data)
@@ -1,13 +1,13 @@
1
1
  """Classification components for qualitative analysis."""
2
2
 
3
3
  from .change_type import ChangeTypeClassifier
4
- from .domain_classifier import DomainClassifier
4
+ from .domain_classifier import DomainClassifier
5
5
  from .intent_analyzer import IntentAnalyzer
6
6
  from .risk_analyzer import RiskAnalyzer
7
7
 
8
8
  __all__ = [
9
9
  "ChangeTypeClassifier",
10
10
  "DomainClassifier",
11
- "IntentAnalyzer",
11
+ "IntentAnalyzer",
12
12
  "RiskAnalyzer",
13
- ]
13
+ ]