devsquad 3.6.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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Worker - Agent Execution Framework
|
|
5
|
+
|
|
6
|
+
Each Worker is an independent Agent instance that executes tasks
|
|
7
|
+
and exchanges information with other Workers via Scratchpad.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
import threading
|
|
14
|
+
from typing import List, Optional, Any, Dict
|
|
15
|
+
|
|
16
|
+
from .models import (
|
|
17
|
+
TaskDefinition,
|
|
18
|
+
WorkerResult,
|
|
19
|
+
TaskNotification,
|
|
20
|
+
ScratchpadEntry,
|
|
21
|
+
EntryType,
|
|
22
|
+
EntryStatus,
|
|
23
|
+
)
|
|
24
|
+
from .scratchpad import Scratchpad
|
|
25
|
+
from .usage_tracker import track_usage
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Worker:
|
|
29
|
+
"""
|
|
30
|
+
Worker Agent Instance - Execution Unit for Multi-Role Collaboration
|
|
31
|
+
|
|
32
|
+
Each Worker represents a specific role (architect/tester/developer, etc.),
|
|
33
|
+
independently executes assigned tasks, and exchanges information with
|
|
34
|
+
other Workers via Scratchpad.
|
|
35
|
+
|
|
36
|
+
Core Capabilities:
|
|
37
|
+
- execute(): Execute TaskDefinition, produce WorkerResult
|
|
38
|
+
- write_finding/question/conflict(): Write different types of info to shared scratchpad
|
|
39
|
+
- send_notification(): Send cross-role messages to other Workers
|
|
40
|
+
- vote_on_proposal(): Participate in consensus voting
|
|
41
|
+
|
|
42
|
+
Lifecycle:
|
|
43
|
+
Create -> Receive Task -> Read Context -> Execute Work -> Write Results -> Send Notifications
|
|
44
|
+
|
|
45
|
+
Relationships with Other Components:
|
|
46
|
+
- Scratchpad: Shared data exchange medium (read/write)
|
|
47
|
+
- ConsensusEngine: Participate in consensus via vote_on_proposal
|
|
48
|
+
- Coordinator: Created and managed by Coordinator.spawn_workers()
|
|
49
|
+
|
|
50
|
+
Usage Example:
|
|
51
|
+
worker = Worker(
|
|
52
|
+
worker_id="arch-abc123",
|
|
53
|
+
role_id="architect",
|
|
54
|
+
role_prompt="You are a system architect...",
|
|
55
|
+
scratchpad=scratchpad,
|
|
56
|
+
)
|
|
57
|
+
result = worker.execute(task_definition)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, worker_id: str, role_id: str, role_prompt: str,
|
|
61
|
+
scratchpad: Scratchpad, llm_backend=None, stream: bool = False):
|
|
62
|
+
"""
|
|
63
|
+
Initialize Worker instance.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
worker_id: Unique worker identifier (format: "{role_id}-{hex}")
|
|
67
|
+
role_id: Role identifier (e.g., "architect", "tester", "solo-coder")
|
|
68
|
+
role_prompt: Role system prompt / instruction template
|
|
69
|
+
scratchpad: Associated shared scratchpad instance
|
|
70
|
+
llm_backend: LLM execution backend (None=MockBackend, returns assembled prompt)
|
|
71
|
+
"""
|
|
72
|
+
self.worker_id = worker_id
|
|
73
|
+
self.role_id = role_id
|
|
74
|
+
self.role_prompt = role_prompt
|
|
75
|
+
self.scratchpad = scratchpad
|
|
76
|
+
self.llm_backend = llm_backend
|
|
77
|
+
self.stream = stream
|
|
78
|
+
self._notifications_outbox: List[TaskNotification] = []
|
|
79
|
+
self._notifications_lock = threading.Lock()
|
|
80
|
+
self._entries_written_count = 0
|
|
81
|
+
self._last_assembled_prompt = None
|
|
82
|
+
|
|
83
|
+
def execute(self, task: TaskDefinition) -> WorkerResult:
|
|
84
|
+
"""
|
|
85
|
+
Execute assigned task.
|
|
86
|
+
|
|
87
|
+
Full execution flow:
|
|
88
|
+
1. Build execution context (read relevant findings from Scratchpad)
|
|
89
|
+
2. Call _do_work() to generate work output
|
|
90
|
+
3. Write output as FINDING to Scratchpad
|
|
91
|
+
4. Return WorkerResult with output and statistics
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
task: Task definition containing description, role ID, phase ID, etc.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
WorkerResult: Execution result containing:
|
|
98
|
+
- success: Whether successful
|
|
99
|
+
- output: Output content dictionary
|
|
100
|
+
- error: Error message (on failure)
|
|
101
|
+
- scratchpad_entries_written: Number of entries written
|
|
102
|
+
- notifications_sent: Number of notifications sent
|
|
103
|
+
- duration_seconds: Execution duration
|
|
104
|
+
|
|
105
|
+
Note:
|
|
106
|
+
Even if _do_work() returns an empty string, execute still returns
|
|
107
|
+
success=True, just with empty output.finding_summary. Only marks
|
|
108
|
+
as failed when an exception is raised.
|
|
109
|
+
"""
|
|
110
|
+
start_time = time.time()
|
|
111
|
+
try:
|
|
112
|
+
context = self._build_execution_context(task)
|
|
113
|
+
|
|
114
|
+
finding = self._do_work(context)
|
|
115
|
+
if finding:
|
|
116
|
+
entry = ScratchpadEntry(
|
|
117
|
+
worker_id=self.worker_id,
|
|
118
|
+
role_id=self.role_id,
|
|
119
|
+
entry_type=EntryType.FINDING,
|
|
120
|
+
content=finding,
|
|
121
|
+
confidence=0.7,
|
|
122
|
+
tags=[task.task_id, task.stage_id or "", "auto"],
|
|
123
|
+
)
|
|
124
|
+
self.write_finding(entry)
|
|
125
|
+
|
|
126
|
+
output = {
|
|
127
|
+
"worker_id": self.worker_id,
|
|
128
|
+
"role_id": self.role_id,
|
|
129
|
+
"task_id": task.task_id,
|
|
130
|
+
"finding_summary": finding,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
result = WorkerResult(
|
|
134
|
+
worker_id=self.worker_id,
|
|
135
|
+
task_id=task.task_id,
|
|
136
|
+
success=True,
|
|
137
|
+
output=output,
|
|
138
|
+
scratchpad_entries_written=self._entries_written_count,
|
|
139
|
+
notifications_sent=len(self._notifications_outbox),
|
|
140
|
+
duration_seconds=time.time() - start_time,
|
|
141
|
+
)
|
|
142
|
+
track_usage(f"worker.{self.role_id}.execute", success=True, metadata={
|
|
143
|
+
"task_id": task.task_id,
|
|
144
|
+
"duration": round(time.time() - start_time, 2)
|
|
145
|
+
})
|
|
146
|
+
return result
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f" [Worker {self.worker_id}] Error: {e}", file=sys.stderr)
|
|
149
|
+
track_usage(f"worker.{self.role_id}.execute", success=False, metadata={
|
|
150
|
+
"task_id": task.task_id,
|
|
151
|
+
"error": str(e)[:100]
|
|
152
|
+
})
|
|
153
|
+
return WorkerResult(
|
|
154
|
+
worker_id=self.worker_id,
|
|
155
|
+
task_id=task.task_id,
|
|
156
|
+
success=False,
|
|
157
|
+
output={"worker_id": self.worker_id, "role_id": self.role_id,
|
|
158
|
+
"task_id": task.task_id, "error_detail": "Execution failed"},
|
|
159
|
+
error=str(e),
|
|
160
|
+
duration_seconds=time.time() - start_time,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def read_scratchpad(self, query: str = "",
|
|
164
|
+
since=None, limit: int = 20) -> List[ScratchpadEntry]:
|
|
165
|
+
"""
|
|
166
|
+
Read relevant entries from shared scratchpad.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
query: Keyword query (fuzzy match in content and tags)
|
|
170
|
+
since: Start time filter (only return entries after this time)
|
|
171
|
+
limit: Maximum number of entries to return
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List[ScratchpadEntry]: Matching entries, sorted by time descending
|
|
175
|
+
"""
|
|
176
|
+
return self.scratchpad.read(
|
|
177
|
+
query=query, since=since, limit=limit,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def write_finding(self, finding: ScratchpadEntry) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Write a finding (FINDING type) to Scratchpad.
|
|
183
|
+
|
|
184
|
+
Automatically sets worker_id and role_id to current Worker identity,
|
|
185
|
+
and increments internal write counter.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
finding: Finding entry to write (ScratchpadEntry instance)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
str: entry_id after writing
|
|
192
|
+
"""
|
|
193
|
+
finding.worker_id = self.worker_id
|
|
194
|
+
finding.role_id = self.role_id
|
|
195
|
+
eid = self.scratchpad.write(finding)
|
|
196
|
+
self._entries_written_count += 1
|
|
197
|
+
return eid
|
|
198
|
+
|
|
199
|
+
def write_question(self, question: str, to_roles: List[str] = None,
|
|
200
|
+
tags: List[str] = None) -> str:
|
|
201
|
+
"""
|
|
202
|
+
Write a question to Scratchpad and optionally notify other roles.
|
|
203
|
+
|
|
204
|
+
Creates a QUESTION type entry and writes to scratchpad. If to_roles
|
|
205
|
+
is specified, also generates TaskNotification for target role Workers.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
question: Question content text
|
|
209
|
+
to_roles: Target role ID list (e.g., ["architect", "tester"]), empty means no notification
|
|
210
|
+
tags: Optional tag list
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
str: entry_id after writing
|
|
214
|
+
"""
|
|
215
|
+
entry = ScratchpadEntry(
|
|
216
|
+
worker_id=self.worker_id,
|
|
217
|
+
role_id=self.role_id,
|
|
218
|
+
entry_type=EntryType.QUESTION,
|
|
219
|
+
content=question,
|
|
220
|
+
confidence=0.5,
|
|
221
|
+
tags=tags or [],
|
|
222
|
+
)
|
|
223
|
+
eid = self.scratchpad.write(entry)
|
|
224
|
+
self._entries_written_count += 1
|
|
225
|
+
|
|
226
|
+
if to_roles:
|
|
227
|
+
notification = TaskNotification(
|
|
228
|
+
from_worker=self.worker_id,
|
|
229
|
+
to_workers=to_roles,
|
|
230
|
+
notification_type="question",
|
|
231
|
+
summary=question[:100],
|
|
232
|
+
details=question,
|
|
233
|
+
action_required="Please answer this question",
|
|
234
|
+
)
|
|
235
|
+
self.send_notification(notification)
|
|
236
|
+
return eid
|
|
237
|
+
|
|
238
|
+
def write_conflict(self, conflict: str, conflicting_entry_id: str,
|
|
239
|
+
reason: str = "") -> str:
|
|
240
|
+
"""
|
|
241
|
+
Write a conflict record (CONFLICT type) to Scratchpad.
|
|
242
|
+
|
|
243
|
+
Called when contradiction with another Worker's output is detected.
|
|
244
|
+
Conflicts trigger subsequent Coordinator.resolve_conflicts() consensus flow.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
conflict: Conflict description text
|
|
248
|
+
conflicting_entry_id: Conflicting peer entry ID
|
|
249
|
+
reason: Conflict reason explanation
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
str: entry_id after writing
|
|
253
|
+
"""
|
|
254
|
+
entry = ScratchpadEntry(
|
|
255
|
+
worker_id=self.worker_id,
|
|
256
|
+
role_id=self.role_id,
|
|
257
|
+
entry_type=EntryType.CONFLICT,
|
|
258
|
+
content=f"{conflict}\n\n[Conflict reason] {reason}",
|
|
259
|
+
confidence=0.8,
|
|
260
|
+
tags=["conflict", conflicting_entry_id],
|
|
261
|
+
)
|
|
262
|
+
eid = self.scratchpad.write(entry)
|
|
263
|
+
self._entries_written_count += 1
|
|
264
|
+
return eid
|
|
265
|
+
|
|
266
|
+
def send_notification(self, notification: TaskNotification):
|
|
267
|
+
"""
|
|
268
|
+
Send cross-Worker notification.
|
|
269
|
+
|
|
270
|
+
Places notification in internal outbox, waiting for Coordinator
|
|
271
|
+
to collect via get_pending_notifications() and forward to target Workers.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
notification: Notification object containing sender, recipient list, summary, etc.
|
|
275
|
+
"""
|
|
276
|
+
with self._notifications_lock:
|
|
277
|
+
self._notifications_outbox.append(notification)
|
|
278
|
+
|
|
279
|
+
def get_pending_notifications(self) -> List[TaskNotification]:
|
|
280
|
+
"""
|
|
281
|
+
Get and clear pending notification queue.
|
|
282
|
+
|
|
283
|
+
Called by Coordinator during collect_results(),
|
|
284
|
+
retrieves all accumulated notifications and clears internal buffer.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List[TaskNotification]: Pending notification list (queue is empty after call)
|
|
288
|
+
"""
|
|
289
|
+
with self._notifications_lock:
|
|
290
|
+
notifications = list(self._notifications_outbox)
|
|
291
|
+
self._notifications_outbox.clear()
|
|
292
|
+
return notifications
|
|
293
|
+
|
|
294
|
+
def get_last_prompt(self) -> Optional[Any]:
|
|
295
|
+
"""
|
|
296
|
+
Get the most recent _do_work() prompt assembly result.
|
|
297
|
+
|
|
298
|
+
Generated by PromptAssembler, contains metadata such as
|
|
299
|
+
complexity/variant/token estimate. Can be used for Skillify
|
|
300
|
+
feedback loop: feed successful prompt variants back to the system.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Optional[AssembledPrompt]: Most recent assembly result, None if never executed
|
|
304
|
+
"""
|
|
305
|
+
return self._last_assembled_prompt
|
|
306
|
+
|
|
307
|
+
def vote_on_proposal(self, proposal_id: str, decision: bool,
|
|
308
|
+
reason: str = "", weight: float = None) -> Dict[str, Any]:
|
|
309
|
+
"""
|
|
310
|
+
Vote on a consensus proposal.
|
|
311
|
+
|
|
312
|
+
Creates a Vote object and wraps it in standard return format.
|
|
313
|
+
Weight defaults from ROLE_WEIGHTS global config by role.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
proposal_id: Consensus proposal ID
|
|
317
|
+
decision: Vote decision (True=approve, False=reject)
|
|
318
|
+
reason: Vote reason explanation
|
|
319
|
+
weight: Vote weight (None uses role default weight)
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Dict[str, Any]: Dictionary containing proposal_id and Vote object
|
|
323
|
+
- proposal_id: Proposal ID
|
|
324
|
+
- vote: Vote data object
|
|
325
|
+
"""
|
|
326
|
+
from .models import Vote, ROLE_WEIGHTS
|
|
327
|
+
w = weight or ROLE_WEIGHTS.get(self.role_id, 1.0)
|
|
328
|
+
vote = Vote(
|
|
329
|
+
voter_id=self.worker_id,
|
|
330
|
+
voter_role=self.role_id,
|
|
331
|
+
decision=decision,
|
|
332
|
+
reason=reason,
|
|
333
|
+
weight=w,
|
|
334
|
+
)
|
|
335
|
+
return {"proposal_id": proposal_id, "vote": vote}
|
|
336
|
+
|
|
337
|
+
def _build_execution_context(self, task: TaskDefinition,
|
|
338
|
+
compression_level=None) -> Dict[str, Any]:
|
|
339
|
+
"""
|
|
340
|
+
Build execution context (including prompt assembly).
|
|
341
|
+
|
|
342
|
+
Reads relevant findings from Scratchpad and performs dynamic prompt
|
|
343
|
+
trimming via PromptAssembler (based on task complexity and compression level).
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
task: Task definition
|
|
347
|
+
compression_level: ContextCompressor compression level (optional, affects prompt style)
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Dict[str, Any]: Execution context containing task/role_prompt/related_findings/
|
|
351
|
+
worker_id/compression_level
|
|
352
|
+
"""
|
|
353
|
+
related = self.read_scratchpad(
|
|
354
|
+
query=task.description[:50], limit=10,
|
|
355
|
+
)
|
|
356
|
+
return {
|
|
357
|
+
"task": task,
|
|
358
|
+
"role_prompt": self.role_prompt,
|
|
359
|
+
"related_findings": [f.content for f in related[:8]],
|
|
360
|
+
"worker_id": self.worker_id,
|
|
361
|
+
"compression_level": compression_level,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
def _do_work(self, context: Dict[str, Any]) -> str:
|
|
365
|
+
"""
|
|
366
|
+
Execute core work - dynamically assemble prompt via PromptAssembler, then execute via LLMBackend.
|
|
367
|
+
|
|
368
|
+
Assembly flow:
|
|
369
|
+
1. Extract task description, related findings, compression level from context
|
|
370
|
+
2. Auto-detect complexity via PromptAssembler.detect_complexity()
|
|
371
|
+
3. Select template variant by complexity (compact/standard/enhanced)
|
|
372
|
+
4. Apply compression level override (if any)
|
|
373
|
+
5. Output final work instruction
|
|
374
|
+
6. If LLMBackend is configured, call LLM; otherwise return assembled instruction
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
context: Execution context built by _build_execution_context()
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
str: LLM response text (with backend) or assembled work instruction text (without backend)
|
|
381
|
+
"""
|
|
382
|
+
from .prompt_assembler import PromptAssembler
|
|
383
|
+
from .llm_backend import MockBackend
|
|
384
|
+
|
|
385
|
+
task = context["task"]
|
|
386
|
+
assembler = PromptAssembler(role_id=self.role_id,
|
|
387
|
+
base_prompt=context["role_prompt"])
|
|
388
|
+
|
|
389
|
+
result = assembler.assemble(
|
|
390
|
+
task_description=task.description,
|
|
391
|
+
related_findings=context.get("related_findings", []),
|
|
392
|
+
task_id=task.task_id,
|
|
393
|
+
compression_level=context.get("compression_level"),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
self._last_assembled_prompt = result
|
|
397
|
+
|
|
398
|
+
backend = self.llm_backend or MockBackend()
|
|
399
|
+
if isinstance(backend, MockBackend):
|
|
400
|
+
from .models import ROLE_REGISTRY
|
|
401
|
+
rdef = ROLE_REGISTRY.get(self.role_id)
|
|
402
|
+
role_name = rdef.name if rdef else self.role_id
|
|
403
|
+
return backend.generate(
|
|
404
|
+
result.instruction,
|
|
405
|
+
role_name=role_name,
|
|
406
|
+
task_description=task.description,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
from .models import ROLE_REGISTRY as _RR
|
|
410
|
+
_rdef = _RR.get(self.role_id)
|
|
411
|
+
_rname = _rdef.name if _rdef else self.role_id
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
from .llm_cache import get_llm_cache
|
|
415
|
+
cache = get_llm_cache()
|
|
416
|
+
cached = cache.get(result.instruction, "backend", getattr(backend, 'model', 'unknown'))
|
|
417
|
+
if cached:
|
|
418
|
+
print(f" [{_rname}] Cache hit.", file=sys.stderr)
|
|
419
|
+
return cached
|
|
420
|
+
except Exception:
|
|
421
|
+
cache = None
|
|
422
|
+
|
|
423
|
+
print(f" [{_rname}] Calling LLM backend...", file=sys.stderr)
|
|
424
|
+
try:
|
|
425
|
+
if self.stream and hasattr(backend, 'generate_stream'):
|
|
426
|
+
print(f" [{_rname}] Streaming...", file=sys.stderr)
|
|
427
|
+
chunks = []
|
|
428
|
+
for chunk in backend.generate_stream(result.instruction):
|
|
429
|
+
print(chunk, end="", flush=True)
|
|
430
|
+
chunks.append(chunk)
|
|
431
|
+
print()
|
|
432
|
+
response = "".join(chunks)
|
|
433
|
+
else:
|
|
434
|
+
response = backend.generate(result.instruction)
|
|
435
|
+
print(f" [{_rname}] Response received.", file=sys.stderr)
|
|
436
|
+
|
|
437
|
+
if cache and response:
|
|
438
|
+
try:
|
|
439
|
+
cache.set(result.instruction, response, "backend", getattr(backend, 'model', 'unknown'))
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
return response
|
|
444
|
+
except Exception as e:
|
|
445
|
+
print(f" [{_rname}] LLM call failed: {e}", file=sys.stderr)
|
|
446
|
+
raise
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class WorkerFactory:
|
|
450
|
+
"""
|
|
451
|
+
Factory class - batch creation of Worker instances.
|
|
452
|
+
|
|
453
|
+
Provides create() and create_batch() creation methods,
|
|
454
|
+
encapsulating Worker instantiation details so callers
|
|
455
|
+
don't need to know internal construction logic.
|
|
456
|
+
|
|
457
|
+
Usage Example:
|
|
458
|
+
single = WorkerFactory.create("w-1", "architect", prompt, scratchpad)
|
|
459
|
+
batch = WorkerFactory.create_batch([
|
|
460
|
+
{"worker_id": "w-1", "role_id": "architect", ...},
|
|
461
|
+
{"worker_id": "w-2", "role_id": "tester", ...},
|
|
462
|
+
], scratchpad)
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
@staticmethod
|
|
466
|
+
def create(worker_id: str, role_id: str, role_prompt: str,
|
|
467
|
+
scratchpad: Scratchpad, llm_backend=None, stream: bool = False) -> Worker:
|
|
468
|
+
"""
|
|
469
|
+
Create a single Worker instance.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
worker_id: Unique worker identifier
|
|
473
|
+
role_id: Role identifier
|
|
474
|
+
role_prompt: Role prompt
|
|
475
|
+
scratchpad: Shared scratchpad instance
|
|
476
|
+
llm_backend: LLM execution backend
|
|
477
|
+
stream: Whether to enable streaming output
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Worker: Newly created Worker instance
|
|
481
|
+
"""
|
|
482
|
+
return Worker(worker_id, role_id, role_prompt, scratchpad, llm_backend, stream=stream)
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def create_batch(workers_config: List[Dict[str, str]],
|
|
486
|
+
scratchpad: Scratchpad, llm_backend=None) -> List[Worker]:
|
|
487
|
+
"""
|
|
488
|
+
Batch create Worker instances.
|
|
489
|
+
|
|
490
|
+
Iterates through config list, creating a Worker for each entry.
|
|
491
|
+
worker_id is auto-generated if not provided (format: "w-{index}").
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
workers_config: Worker config list, each entry contains:
|
|
495
|
+
- worker_id (optional): Worker ID
|
|
496
|
+
- role_id (required): Role identifier
|
|
497
|
+
- role_prompt (optional): Role prompt
|
|
498
|
+
scratchpad: Shared Scratchpad instance for all Workers
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
List[Worker]: List of created Worker instances
|
|
502
|
+
"""
|
|
503
|
+
workers = []
|
|
504
|
+
for cfg in workers_config:
|
|
505
|
+
w = WorkerFactory.create(
|
|
506
|
+
worker_id=cfg.get("worker_id", f"w-{len(workers)}"),
|
|
507
|
+
role_id=cfg["role_id"],
|
|
508
|
+
role_prompt=cfg.get("role_prompt", ""),
|
|
509
|
+
scratchpad=scratchpad,
|
|
510
|
+
llm_backend=llm_backend,
|
|
511
|
+
)
|
|
512
|
+
workers.append(w)
|
|
513
|
+
return workers
|