codeframe-ai 0.9.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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""Task Scheduler Component for Intelligent Task Scheduling.
|
|
2
|
+
|
|
3
|
+
This module provides scheduling capabilities for task management:
|
|
4
|
+
- schedule_tasks() - Assign start times based on dependencies and resources
|
|
5
|
+
- optimize_schedule() - Use heuristics to minimize project duration
|
|
6
|
+
- predict_completion_date() - Estimate project end date
|
|
7
|
+
- identify_bottlenecks() - Find tasks/agents causing delays
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from typing import Dict, List, Optional, Any
|
|
14
|
+
|
|
15
|
+
from codeframe.agents.dependency_resolver import DependencyResolver
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TaskAssignment:
|
|
22
|
+
"""Assignment of a task to a time slot and agent."""
|
|
23
|
+
|
|
24
|
+
task_id: int
|
|
25
|
+
start_time: float # Hours from project start
|
|
26
|
+
end_time: float # Hours from project start
|
|
27
|
+
assigned_agent: Optional[int] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TimelineEntry:
|
|
32
|
+
"""Entry in the project timeline."""
|
|
33
|
+
|
|
34
|
+
time: float
|
|
35
|
+
event_type: str # "start", "end"
|
|
36
|
+
task_id: int
|
|
37
|
+
agent_id: Optional[int] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ScheduleResult:
|
|
42
|
+
"""Result of task scheduling."""
|
|
43
|
+
|
|
44
|
+
task_assignments: Dict[int, TaskAssignment]
|
|
45
|
+
total_duration: float
|
|
46
|
+
timeline: List[TimelineEntry]
|
|
47
|
+
agents_used: int = 1
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ScheduleOptimization:
|
|
52
|
+
"""Result of schedule optimization."""
|
|
53
|
+
|
|
54
|
+
optimized_schedule: "ScheduleResult"
|
|
55
|
+
improvement_percentage: float
|
|
56
|
+
changes_made: List[str]
|
|
57
|
+
original_duration: float
|
|
58
|
+
optimized_duration: float
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CompletionPrediction:
|
|
63
|
+
"""Prediction for project completion."""
|
|
64
|
+
|
|
65
|
+
predicted_date: datetime
|
|
66
|
+
confidence_interval: Dict[str, datetime]
|
|
67
|
+
remaining_hours: float
|
|
68
|
+
completed_percentage: float
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class BottleneckInfo:
|
|
73
|
+
"""Information about a scheduling bottleneck."""
|
|
74
|
+
|
|
75
|
+
task_id: int
|
|
76
|
+
bottleneck_type: str # "duration", "dependencies", "resource"
|
|
77
|
+
impact_hours: float
|
|
78
|
+
recommendation: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TaskScheduler:
|
|
82
|
+
"""
|
|
83
|
+
Intelligent task scheduler using Critical Path Method (CPM).
|
|
84
|
+
|
|
85
|
+
Capabilities:
|
|
86
|
+
- Schedule tasks based on dependencies and resource constraints
|
|
87
|
+
- Optimize schedules to minimize project duration
|
|
88
|
+
- Predict completion dates with confidence intervals
|
|
89
|
+
- Identify scheduling bottlenecks
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self):
|
|
93
|
+
"""Initialize the task scheduler."""
|
|
94
|
+
# Default uncertainty factor for predictions (±20%)
|
|
95
|
+
self.uncertainty_factor = 0.2
|
|
96
|
+
|
|
97
|
+
def schedule_tasks(
|
|
98
|
+
self,
|
|
99
|
+
tasks: List[Any],
|
|
100
|
+
task_durations: Dict[int, float],
|
|
101
|
+
resolver: DependencyResolver,
|
|
102
|
+
agents_available: int = 1,
|
|
103
|
+
) -> ScheduleResult:
|
|
104
|
+
"""
|
|
105
|
+
Schedule tasks based on dependencies and available resources.
|
|
106
|
+
|
|
107
|
+
Uses Critical Path Method to assign start/end times while
|
|
108
|
+
respecting dependency constraints and agent availability.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
tasks: List of Task objects
|
|
112
|
+
task_durations: Dict mapping task_id to duration in hours
|
|
113
|
+
resolver: DependencyResolver with built dependency graph
|
|
114
|
+
agents_available: Number of parallel agents/workers
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
ScheduleResult with task assignments and timeline
|
|
118
|
+
"""
|
|
119
|
+
# Get parallel execution waves
|
|
120
|
+
waves = resolver.identify_parallel_opportunities()
|
|
121
|
+
|
|
122
|
+
# Build assignments
|
|
123
|
+
task_assignments: Dict[int, TaskAssignment] = {}
|
|
124
|
+
timeline: List[TimelineEntry] = []
|
|
125
|
+
|
|
126
|
+
# Track agent availability
|
|
127
|
+
agent_end_times = [0.0] * agents_available
|
|
128
|
+
|
|
129
|
+
# Process tasks wave by wave
|
|
130
|
+
for wave_num in sorted(waves.keys()):
|
|
131
|
+
wave_tasks = waves[wave_num]
|
|
132
|
+
|
|
133
|
+
# Sort by duration (longer tasks first for better packing)
|
|
134
|
+
wave_tasks_sorted = sorted(
|
|
135
|
+
wave_tasks,
|
|
136
|
+
key=lambda tid: task_durations.get(tid, 0),
|
|
137
|
+
reverse=True,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
for task_id in wave_tasks_sorted:
|
|
141
|
+
duration = task_durations.get(task_id, 0.0)
|
|
142
|
+
|
|
143
|
+
# Find earliest start time based on dependencies
|
|
144
|
+
deps = resolver.dependencies.get(task_id, set())
|
|
145
|
+
earliest_start = 0.0
|
|
146
|
+
if deps:
|
|
147
|
+
for dep_id in deps:
|
|
148
|
+
if dep_id in task_assignments:
|
|
149
|
+
earliest_start = max(
|
|
150
|
+
earliest_start,
|
|
151
|
+
task_assignments[dep_id].end_time,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Find the first available agent
|
|
155
|
+
best_agent = 0
|
|
156
|
+
best_start = max(earliest_start, agent_end_times[0])
|
|
157
|
+
|
|
158
|
+
for agent_idx, agent_end in enumerate(agent_end_times):
|
|
159
|
+
potential_start = max(earliest_start, agent_end)
|
|
160
|
+
if potential_start < best_start:
|
|
161
|
+
best_start = potential_start
|
|
162
|
+
best_agent = agent_idx
|
|
163
|
+
|
|
164
|
+
# Create assignment
|
|
165
|
+
end_time = best_start + duration
|
|
166
|
+
assignment = TaskAssignment(
|
|
167
|
+
task_id=task_id,
|
|
168
|
+
start_time=best_start,
|
|
169
|
+
end_time=end_time,
|
|
170
|
+
assigned_agent=best_agent,
|
|
171
|
+
)
|
|
172
|
+
task_assignments[task_id] = assignment
|
|
173
|
+
|
|
174
|
+
# Update agent availability
|
|
175
|
+
agent_end_times[best_agent] = end_time
|
|
176
|
+
|
|
177
|
+
# Add timeline entries
|
|
178
|
+
timeline.append(
|
|
179
|
+
TimelineEntry(
|
|
180
|
+
time=best_start,
|
|
181
|
+
event_type="start",
|
|
182
|
+
task_id=task_id,
|
|
183
|
+
agent_id=best_agent,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
timeline.append(
|
|
187
|
+
TimelineEntry(
|
|
188
|
+
time=end_time,
|
|
189
|
+
event_type="end",
|
|
190
|
+
task_id=task_id,
|
|
191
|
+
agent_id=best_agent,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Sort timeline by time
|
|
196
|
+
timeline.sort(key=lambda e: (e.time, 0 if e.event_type == "start" else 1))
|
|
197
|
+
|
|
198
|
+
# Warn if any tasks were not scheduled (e.g., due to unknown dependency IDs)
|
|
199
|
+
expected_task_ids = set(task_durations.keys())
|
|
200
|
+
scheduled_task_ids = set(task_assignments.keys())
|
|
201
|
+
missing_task_ids = expected_task_ids - scheduled_task_ids
|
|
202
|
+
if missing_task_ids:
|
|
203
|
+
logger.warning(
|
|
204
|
+
f"Tasks omitted from schedule (possibly due to unknown dependencies): "
|
|
205
|
+
f"{sorted(missing_task_ids)}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Calculate total duration
|
|
209
|
+
total_duration = max(
|
|
210
|
+
(a.end_time for a in task_assignments.values()),
|
|
211
|
+
default=0.0,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return ScheduleResult(
|
|
215
|
+
task_assignments=task_assignments,
|
|
216
|
+
total_duration=total_duration,
|
|
217
|
+
timeline=timeline,
|
|
218
|
+
agents_used=agents_available,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def optimize_schedule(
|
|
222
|
+
self,
|
|
223
|
+
schedule: ScheduleResult,
|
|
224
|
+
resolver: DependencyResolver,
|
|
225
|
+
constraints: Optional[Dict[str, Any]] = None,
|
|
226
|
+
) -> ScheduleOptimization:
|
|
227
|
+
"""
|
|
228
|
+
Optimize a schedule to minimize project duration.
|
|
229
|
+
|
|
230
|
+
Uses heuristics to identify parallelization opportunities
|
|
231
|
+
and suggest task reordering within slack windows.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
schedule: Current ScheduleResult to optimize
|
|
235
|
+
resolver: DependencyResolver with dependency graph
|
|
236
|
+
constraints: Optional constraints (max_parallel, etc.)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
ScheduleOptimization with optimized schedule and improvements
|
|
240
|
+
"""
|
|
241
|
+
constraints = constraints or {}
|
|
242
|
+
max_parallel = constraints.get("max_parallel", schedule.agents_used)
|
|
243
|
+
|
|
244
|
+
changes_made: List[str] = []
|
|
245
|
+
original_duration = schedule.total_duration
|
|
246
|
+
|
|
247
|
+
# If we can use more agents than currently scheduled, re-schedule
|
|
248
|
+
if max_parallel > schedule.agents_used:
|
|
249
|
+
# Extract task durations from current assignments
|
|
250
|
+
task_durations = {
|
|
251
|
+
tid: assignment.end_time - assignment.start_time
|
|
252
|
+
for tid, assignment in schedule.task_assignments.items()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Get task list from assignments
|
|
256
|
+
task_ids = list(schedule.task_assignments.keys())
|
|
257
|
+
|
|
258
|
+
# Re-schedule with more agents
|
|
259
|
+
optimized = self._reschedule_with_agents(
|
|
260
|
+
task_ids=task_ids,
|
|
261
|
+
task_durations=task_durations,
|
|
262
|
+
resolver=resolver,
|
|
263
|
+
agents_available=max_parallel,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if optimized.total_duration < original_duration:
|
|
267
|
+
changes_made.append(
|
|
268
|
+
f"Increased parallelization from {schedule.agents_used} "
|
|
269
|
+
f"to {max_parallel} agents"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
improvement = (
|
|
273
|
+
(original_duration - optimized.total_duration) / original_duration * 100
|
|
274
|
+
if original_duration > 0
|
|
275
|
+
else 0
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return ScheduleOptimization(
|
|
279
|
+
optimized_schedule=optimized,
|
|
280
|
+
improvement_percentage=improvement,
|
|
281
|
+
changes_made=changes_made,
|
|
282
|
+
original_duration=original_duration,
|
|
283
|
+
optimized_duration=optimized.total_duration,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# No optimization possible
|
|
287
|
+
return ScheduleOptimization(
|
|
288
|
+
optimized_schedule=schedule,
|
|
289
|
+
improvement_percentage=0.0,
|
|
290
|
+
changes_made=["No optimizations found"],
|
|
291
|
+
original_duration=original_duration,
|
|
292
|
+
optimized_duration=original_duration,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def _reschedule_with_agents(
|
|
296
|
+
self,
|
|
297
|
+
task_ids: List[int],
|
|
298
|
+
task_durations: Dict[int, float],
|
|
299
|
+
resolver: DependencyResolver,
|
|
300
|
+
agents_available: int,
|
|
301
|
+
) -> ScheduleResult:
|
|
302
|
+
"""Re-schedule tasks with a different number of agents."""
|
|
303
|
+
# Get parallel waves
|
|
304
|
+
waves = resolver.identify_parallel_opportunities()
|
|
305
|
+
|
|
306
|
+
task_assignments: Dict[int, TaskAssignment] = {}
|
|
307
|
+
timeline: List[TimelineEntry] = []
|
|
308
|
+
agent_end_times = [0.0] * agents_available
|
|
309
|
+
|
|
310
|
+
for wave_num in sorted(waves.keys()):
|
|
311
|
+
wave_tasks = [tid for tid in waves[wave_num] if tid in task_ids]
|
|
312
|
+
wave_tasks_sorted = sorted(
|
|
313
|
+
wave_tasks,
|
|
314
|
+
key=lambda tid: task_durations.get(tid, 0),
|
|
315
|
+
reverse=True,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
for task_id in wave_tasks_sorted:
|
|
319
|
+
duration = task_durations.get(task_id, 0.0)
|
|
320
|
+
|
|
321
|
+
# Find earliest start based on dependencies
|
|
322
|
+
deps = resolver.dependencies.get(task_id, set())
|
|
323
|
+
earliest_start = 0.0
|
|
324
|
+
for dep_id in deps:
|
|
325
|
+
if dep_id in task_assignments:
|
|
326
|
+
earliest_start = max(
|
|
327
|
+
earliest_start,
|
|
328
|
+
task_assignments[dep_id].end_time,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Find best agent
|
|
332
|
+
best_agent = 0
|
|
333
|
+
best_start = max(earliest_start, agent_end_times[0])
|
|
334
|
+
|
|
335
|
+
for agent_idx, agent_end in enumerate(agent_end_times):
|
|
336
|
+
potential_start = max(earliest_start, agent_end)
|
|
337
|
+
if potential_start < best_start:
|
|
338
|
+
best_start = potential_start
|
|
339
|
+
best_agent = agent_idx
|
|
340
|
+
|
|
341
|
+
end_time = best_start + duration
|
|
342
|
+
task_assignments[task_id] = TaskAssignment(
|
|
343
|
+
task_id=task_id,
|
|
344
|
+
start_time=best_start,
|
|
345
|
+
end_time=end_time,
|
|
346
|
+
assigned_agent=best_agent,
|
|
347
|
+
)
|
|
348
|
+
agent_end_times[best_agent] = end_time
|
|
349
|
+
|
|
350
|
+
timeline.append(
|
|
351
|
+
TimelineEntry(
|
|
352
|
+
time=best_start,
|
|
353
|
+
event_type="start",
|
|
354
|
+
task_id=task_id,
|
|
355
|
+
agent_id=best_agent,
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
timeline.append(
|
|
359
|
+
TimelineEntry(
|
|
360
|
+
time=end_time,
|
|
361
|
+
event_type="end",
|
|
362
|
+
task_id=task_id,
|
|
363
|
+
agent_id=best_agent,
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
timeline.sort(key=lambda e: e.time)
|
|
368
|
+
total_duration = max(
|
|
369
|
+
(a.end_time for a in task_assignments.values()), default=0.0
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return ScheduleResult(
|
|
373
|
+
task_assignments=task_assignments,
|
|
374
|
+
total_duration=total_duration,
|
|
375
|
+
timeline=timeline,
|
|
376
|
+
agents_used=agents_available,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def predict_completion_date(
|
|
380
|
+
self,
|
|
381
|
+
schedule: ScheduleResult,
|
|
382
|
+
current_progress: Dict[int, str],
|
|
383
|
+
start_date: datetime,
|
|
384
|
+
hours_per_day: float = 8.0,
|
|
385
|
+
) -> CompletionPrediction:
|
|
386
|
+
"""
|
|
387
|
+
Predict project completion date based on schedule and progress.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
schedule: Current ScheduleResult
|
|
391
|
+
current_progress: Dict mapping task_id to status ("completed", "in_progress")
|
|
392
|
+
start_date: Project start date
|
|
393
|
+
hours_per_day: Working hours per day (default 8)
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
CompletionPrediction with predicted date and confidence interval
|
|
397
|
+
"""
|
|
398
|
+
# Calculate remaining hours
|
|
399
|
+
total_hours = schedule.total_duration
|
|
400
|
+
completed_hours = 0.0
|
|
401
|
+
|
|
402
|
+
for task_id, status in current_progress.items():
|
|
403
|
+
if status == "completed" and task_id in schedule.task_assignments:
|
|
404
|
+
assignment = schedule.task_assignments[task_id]
|
|
405
|
+
completed_hours += assignment.end_time - assignment.start_time
|
|
406
|
+
|
|
407
|
+
remaining_hours = max(0, total_hours - completed_hours)
|
|
408
|
+
completed_percentage = (
|
|
409
|
+
(completed_hours / total_hours * 100) if total_hours > 0 else 100
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Calculate working days needed
|
|
413
|
+
days_needed = remaining_hours / hours_per_day
|
|
414
|
+
|
|
415
|
+
# Apply uncertainty for confidence interval
|
|
416
|
+
early_days = days_needed * (1 - self.uncertainty_factor)
|
|
417
|
+
late_days = days_needed * (1 + self.uncertainty_factor)
|
|
418
|
+
|
|
419
|
+
predicted_date = start_date + timedelta(days=days_needed)
|
|
420
|
+
early_date = start_date + timedelta(days=early_days)
|
|
421
|
+
late_date = start_date + timedelta(days=late_days)
|
|
422
|
+
|
|
423
|
+
return CompletionPrediction(
|
|
424
|
+
predicted_date=predicted_date,
|
|
425
|
+
confidence_interval={
|
|
426
|
+
"early": early_date,
|
|
427
|
+
"late": late_date,
|
|
428
|
+
},
|
|
429
|
+
remaining_hours=remaining_hours,
|
|
430
|
+
completed_percentage=completed_percentage,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def identify_bottlenecks(
|
|
434
|
+
self,
|
|
435
|
+
schedule: ScheduleResult,
|
|
436
|
+
task_durations: Dict[int, float],
|
|
437
|
+
resolver: DependencyResolver,
|
|
438
|
+
) -> List[BottleneckInfo]:
|
|
439
|
+
"""
|
|
440
|
+
Identify scheduling bottlenecks in the project.
|
|
441
|
+
|
|
442
|
+
Identifies:
|
|
443
|
+
- Long duration tasks on critical path
|
|
444
|
+
- Tasks with many dependents causing delays
|
|
445
|
+
- Resource constraints limiting parallelization
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
schedule: Current ScheduleResult
|
|
449
|
+
task_durations: Dict mapping task_id to duration
|
|
450
|
+
resolver: DependencyResolver with dependency graph
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
List of BottleneckInfo objects
|
|
454
|
+
"""
|
|
455
|
+
bottlenecks: List[BottleneckInfo] = []
|
|
456
|
+
|
|
457
|
+
# Get critical path
|
|
458
|
+
cp_result = resolver.calculate_critical_path(task_durations)
|
|
459
|
+
critical_set = set(cp_result.critical_task_ids)
|
|
460
|
+
|
|
461
|
+
# Identify long duration tasks on critical path
|
|
462
|
+
avg_duration = (
|
|
463
|
+
sum(task_durations.values()) / len(task_durations)
|
|
464
|
+
if task_durations
|
|
465
|
+
else 0
|
|
466
|
+
)
|
|
467
|
+
duration_threshold = avg_duration * 2 # Tasks > 2x average
|
|
468
|
+
|
|
469
|
+
for task_id in critical_set:
|
|
470
|
+
duration = task_durations.get(task_id, 0)
|
|
471
|
+
|
|
472
|
+
if duration > duration_threshold:
|
|
473
|
+
dependent_count = len(resolver.dependents.get(task_id, set()))
|
|
474
|
+
impact = duration - avg_duration
|
|
475
|
+
|
|
476
|
+
bottlenecks.append(
|
|
477
|
+
BottleneckInfo(
|
|
478
|
+
task_id=task_id,
|
|
479
|
+
bottleneck_type="duration",
|
|
480
|
+
impact_hours=impact,
|
|
481
|
+
recommendation=(
|
|
482
|
+
f"Task {task_id} takes {duration:.1f}h (2x average). "
|
|
483
|
+
f"Consider splitting into smaller tasks. "
|
|
484
|
+
f"Blocks {dependent_count} downstream tasks."
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Identify tasks with many dependents
|
|
490
|
+
dependent_threshold = 3
|
|
491
|
+
for task_id in resolver.all_tasks:
|
|
492
|
+
dependent_count = len(resolver.dependents.get(task_id, set()))
|
|
493
|
+
|
|
494
|
+
if dependent_count >= dependent_threshold and task_id in critical_set:
|
|
495
|
+
duration = task_durations.get(task_id, 0)
|
|
496
|
+
# Impact is duration times number of blocked tasks
|
|
497
|
+
impact = duration * (dependent_count - 1)
|
|
498
|
+
|
|
499
|
+
bottlenecks.append(
|
|
500
|
+
BottleneckInfo(
|
|
501
|
+
task_id=task_id,
|
|
502
|
+
bottleneck_type="dependencies",
|
|
503
|
+
impact_hours=impact,
|
|
504
|
+
recommendation=(
|
|
505
|
+
f"Task {task_id} blocks {dependent_count} tasks. "
|
|
506
|
+
f"Prioritize this task or consider parallelizing dependents."
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
return bottlenecks
|