pulse-coding-agent 0.1.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.
- pulse/__init__.py +5 -0
- pulse/__main__.py +4 -0
- pulse/agent.py +270 -0
- pulse/agent_manager.py +335 -0
- pulse/audit.py +70 -0
- pulse/auth.py +670 -0
- pulse/ci/github_client.py +66 -0
- pulse/ci/runner.py +28 -0
- pulse/cli.py +1075 -0
- pulse/cli_ui.py +977 -0
- pulse/config.py +167 -0
- pulse/context.py +960 -0
- pulse/conversations/__init__.py +8 -0
- pulse/conversations/manager.py +312 -0
- pulse/core/agent.py +188 -0
- pulse/core/planner.py +105 -0
- pulse/core/protocols.py +37 -0
- pulse/edits.py +65 -0
- pulse/episodic.py +93 -0
- pulse/eval/__init__.py +8 -0
- pulse/eval/trajectory_logger.py +91 -0
- pulse/eval/verifier.py +133 -0
- pulse/execution/__init__.py +5 -0
- pulse/execution/remote_task.py +76 -0
- pulse/git.py +162 -0
- pulse/interactive.py +234 -0
- pulse/mcp/__init__.py +4 -0
- pulse/mcp/client.py +215 -0
- pulse/mcp/local_tools.py +105 -0
- pulse/memory.py +212 -0
- pulse/mutations.py +283 -0
- pulse/orchestration/__init__.py +3 -0
- pulse/orchestration/orchestrator.py +162 -0
- pulse/patch.py +129 -0
- pulse/planner/__init__.py +3 -0
- pulse/planner/dag_planner.py +85 -0
- pulse/planner/execution_loop.py +159 -0
- pulse/production.py +235 -0
- pulse/provider.py +59 -0
- pulse/provider_keys.py +278 -0
- pulse/providers/__init__.py +26 -0
- pulse/providers/anthropic.py +65 -0
- pulse/providers/base.py +251 -0
- pulse/providers/deepseek.py +10 -0
- pulse/providers/failover.py +32 -0
- pulse/providers/gemini.py +66 -0
- pulse/providers/groq.py +10 -0
- pulse/providers/manager.py +262 -0
- pulse/providers/openai.py +40 -0
- pulse/providers/openrouter.py +20 -0
- pulse/py.typed +1 -0
- pulse/reasoning.py +570 -0
- pulse/refactor/__init__.py +3 -0
- pulse/refactor/impact_analyzer.py +44 -0
- pulse/repository.py +209 -0
- pulse/rpc.py +249 -0
- pulse/rule_synthesizer.py +54 -0
- pulse/runtime.py +217 -0
- pulse/safety/__init__.py +3 -0
- pulse/safety/safety_manager.py +97 -0
- pulse/sandbox/SECURITY.md +57 -0
- pulse/sandbox/__init__.py +57 -0
- pulse/sandbox/api.py +594 -0
- pulse/sandbox/audit.py +153 -0
- pulse/sandbox/backend/__init__.py +7 -0
- pulse/sandbox/backend/base.py +72 -0
- pulse/sandbox/backend/docker.py +498 -0
- pulse/sandbox/backend/host.py +140 -0
- pulse/sandbox/backend/remote.py +224 -0
- pulse/sandbox/errors.py +106 -0
- pulse/sandbox/filesystem.py +476 -0
- pulse/sandbox/git_safe.py +50 -0
- pulse/sandbox/lifecycle.py +88 -0
- pulse/sandbox/network.py +205 -0
- pulse/sandbox/path_validator.py +280 -0
- pulse/sandbox/policy.py +209 -0
- pulse/sandbox/process.py +331 -0
- pulse/sandbox/project.py +158 -0
- pulse/sandbox/python_safe.py +62 -0
- pulse/sandbox/remote/__init__.py +1 -0
- pulse/sandbox/remote/client.py +389 -0
- pulse/sandbox/remote/models.py +167 -0
- pulse/sandbox/remote/protocol.py +65 -0
- pulse/sandbox/remote/server.py +984 -0
- pulse/sandbox/remote/worker.py +175 -0
- pulse/sandbox/resources.py +236 -0
- pulse/sandbox/secrets.py +241 -0
- pulse/session_manager.py +365 -0
- pulse/software_engineer.py +189 -0
- pulse/storage.py +140 -0
- pulse/streaming.py +385 -0
- pulse/subprocesses.py +79 -0
- pulse/task_manager.py +2005 -0
- pulse/telemetry/__init__.py +25 -0
- pulse/telemetry/cost_tracker.py +95 -0
- pulse/telemetry/logger.py +110 -0
- pulse/tool_policy.py +197 -0
- pulse/tool_registry.py +163 -0
- pulse/tools.py +372 -0
- pulse/verification.py +118 -0
- pulse_coding_agent-0.1.0.dist-info/METADATA +211 -0
- pulse_coding_agent-0.1.0.dist-info/RECORD +104 -0
- pulse_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- pulse_coding_agent-0.1.0.dist-info/entry_points.txt +4 -0
pulse/reasoning.py
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""Modular Reasoning Engine for Pulse.
|
|
2
|
+
|
|
3
|
+
Provides pre-execution intent analysis, risk-aware strategy selection, explicit
|
|
4
|
+
sanitized reasoning step generation (without chain-of-thought exposure),
|
|
5
|
+
integration with ContextManager, Planner, SafetyManager, and ToolRegistry,
|
|
6
|
+
along with retries, confidence scoring, and heuristic fallback logic.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from pulse.core.planner import ExecutionPlan, PlanningRequest, RequestPlanner
|
|
21
|
+
from pulse.core.protocols import LLMProvider
|
|
22
|
+
from pulse.safety.safety_manager import RiskLevel, SafetyManager
|
|
23
|
+
from pulse.tool_registry import ToolRegistry
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Enums and Data Models
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IntentCategory(Enum):
|
|
34
|
+
"""Primary intent categories for user requests."""
|
|
35
|
+
|
|
36
|
+
DIRECT_ANSWER = "direct_answer"
|
|
37
|
+
TOOL_EXECUTION = "tool_execution"
|
|
38
|
+
FILE_EDIT = "file_edit"
|
|
39
|
+
REPOSITORY_SEARCH = "repository_search"
|
|
40
|
+
CLARIFICATION_NEEDED = "clarification_needed"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ExecutionStrategyType(Enum):
|
|
44
|
+
"""Optimal execution strategy selected for fulfilling a request."""
|
|
45
|
+
|
|
46
|
+
DIRECT_RESPONSE = "direct_response"
|
|
47
|
+
PLANNED_TOOL_EXECUTION = "planned_tool_execution"
|
|
48
|
+
RECURSIVE_DECOMPOSITION = "recursive_decomposition"
|
|
49
|
+
SAFE_USER_CONFIRMATION = "safe_user_confirmation"
|
|
50
|
+
CLARIFICATION_REQUEST = "clarification_request"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(slots=True)
|
|
54
|
+
class IntentAnalysis:
|
|
55
|
+
"""Structured assessment of user prompt intent."""
|
|
56
|
+
|
|
57
|
+
category: IntentCategory
|
|
58
|
+
summary: str
|
|
59
|
+
confidence_score: float
|
|
60
|
+
detected_entities: list[str] = field(default_factory=list)
|
|
61
|
+
risk_level: RiskLevel = RiskLevel.LOW
|
|
62
|
+
ambiguities: list[str] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(slots=True)
|
|
66
|
+
class ReasoningStep:
|
|
67
|
+
"""One explicit, public reasoning step.
|
|
68
|
+
|
|
69
|
+
Internal LLM scratchpads / raw chain-of-thought tokens are stripped.
|
|
70
|
+
Only clean, high-level rationale is stored.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
step_number: int
|
|
74
|
+
title: str
|
|
75
|
+
action: str
|
|
76
|
+
rationale: str
|
|
77
|
+
status: str = "completed"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(slots=True)
|
|
81
|
+
class ExecutionStrategy:
|
|
82
|
+
"""Strategy configuration chosen by the Reasoning Engine."""
|
|
83
|
+
|
|
84
|
+
strategy_type: ExecutionStrategyType
|
|
85
|
+
steps: list[ReasoningStep]
|
|
86
|
+
estimated_risk: RiskLevel
|
|
87
|
+
requires_planning: bool
|
|
88
|
+
requires_safety_approval: bool
|
|
89
|
+
selected_tools: list[str] = field(default_factory=list)
|
|
90
|
+
confidence: float = 1.0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(slots=True)
|
|
94
|
+
class ReasoningResult:
|
|
95
|
+
"""Final output object from the Reasoning Engine."""
|
|
96
|
+
|
|
97
|
+
intent: IntentAnalysis
|
|
98
|
+
strategy: ExecutionStrategy
|
|
99
|
+
reasoning_steps: list[ReasoningStep]
|
|
100
|
+
response_text: str | None = None
|
|
101
|
+
execution_plan: ExecutionPlan | Any | None = None
|
|
102
|
+
overall_confidence: float = 1.0
|
|
103
|
+
retries_used: int = 0
|
|
104
|
+
used_fallback: bool = False
|
|
105
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Reasoning Engine
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ReasoningEngine:
|
|
114
|
+
"""Provider-agnostic, async Reasoning Engine for Pulse.
|
|
115
|
+
|
|
116
|
+
Coordinates intent classification, risk assessment, context gathering,
|
|
117
|
+
tool discovery, execution planning, and response formulation with full
|
|
118
|
+
retry capability and deterministic heuristic fallbacks.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
provider: LLMProvider | Any | None = None,
|
|
125
|
+
context_manager: Any | None = None,
|
|
126
|
+
planner: Any | None = None,
|
|
127
|
+
safety_manager: SafetyManager | None = None,
|
|
128
|
+
tool_registry: ToolRegistry | None = None,
|
|
129
|
+
max_retries: int = 3,
|
|
130
|
+
confidence_threshold: float = 0.6,
|
|
131
|
+
) -> None:
|
|
132
|
+
self.provider = provider
|
|
133
|
+
self.context_manager = context_manager
|
|
134
|
+
self.planner = planner or RequestPlanner()
|
|
135
|
+
self.safety_manager = safety_manager or SafetyManager()
|
|
136
|
+
self.tool_registry = tool_registry
|
|
137
|
+
self.max_retries = max_retries
|
|
138
|
+
self.confidence_threshold = confidence_threshold
|
|
139
|
+
|
|
140
|
+
async def reason(
|
|
141
|
+
self,
|
|
142
|
+
request: str,
|
|
143
|
+
*,
|
|
144
|
+
active_file: str | None = None,
|
|
145
|
+
history: Sequence[Any] | None = None,
|
|
146
|
+
) -> ReasoningResult:
|
|
147
|
+
"""Main async entrypoint for analyzing and reasoning over a request."""
|
|
148
|
+
clean_request = request.strip()
|
|
149
|
+
if not clean_request:
|
|
150
|
+
return self._empty_request_result()
|
|
151
|
+
|
|
152
|
+
# 1. Gather context from ContextManager if available
|
|
153
|
+
context_strings: list[str] = []
|
|
154
|
+
if self.context_manager and hasattr(self.context_manager, "as_strings"):
|
|
155
|
+
try:
|
|
156
|
+
context_strings = await self.context_manager.as_strings(
|
|
157
|
+
clean_request, active_file=active_file
|
|
158
|
+
)
|
|
159
|
+
# Intentionally broad to isolate execution boundaries and prevent crashes.
|
|
160
|
+
except Exception as err: # noqa: BLE001
|
|
161
|
+
logger.warning(f"ContextManager gather failed: {err}")
|
|
162
|
+
|
|
163
|
+
# 2. Analyze intent with retry & fallback
|
|
164
|
+
intent, retries_used, used_fallback = await self._analyze_intent_with_fallback(
|
|
165
|
+
clean_request, context_strings
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# 3. Select optimal execution strategy
|
|
169
|
+
strategy = await self.select_strategy(intent, context_strings)
|
|
170
|
+
|
|
171
|
+
# 4. Formulate explicit sanitized reasoning steps
|
|
172
|
+
reasoning_steps = self._sanitize_reasoning_steps(strategy.steps)
|
|
173
|
+
|
|
174
|
+
# 5. Build ExecutionPlan if strategy requires planning
|
|
175
|
+
execution_plan: ExecutionPlan | Any | None = None
|
|
176
|
+
if strategy.requires_planning and self.planner:
|
|
177
|
+
try:
|
|
178
|
+
planning_req = PlanningRequest(
|
|
179
|
+
message=clean_request,
|
|
180
|
+
metadata={
|
|
181
|
+
"category": intent.category.value,
|
|
182
|
+
"risk": intent.risk_level.value,
|
|
183
|
+
"tools": strategy.selected_tools,
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
if hasattr(self.planner, "plan"):
|
|
187
|
+
res = self.planner.plan(planning_req)
|
|
188
|
+
execution_plan = await res if asyncio.iscoroutine(res) else res
|
|
189
|
+
# Intentionally broad to isolate execution boundaries and prevent crashes.
|
|
190
|
+
except Exception as err: # noqa: BLE001
|
|
191
|
+
logger.warning(f"Planner failed during reasoning: {err}")
|
|
192
|
+
|
|
193
|
+
# 6. Formulate immediate direct response text if strategy is Direct Answer or Clarification
|
|
194
|
+
response_text: str | None = None
|
|
195
|
+
if strategy.strategy_type == ExecutionStrategyType.CLARIFICATION_REQUEST:
|
|
196
|
+
response_text = self._format_clarification_prompt(clean_request, intent)
|
|
197
|
+
elif strategy.strategy_type == ExecutionStrategyType.DIRECT_RESPONSE and self.provider:
|
|
198
|
+
response_text = await self._generate_direct_response(clean_request, context_strings)
|
|
199
|
+
|
|
200
|
+
overall_confidence = round(
|
|
201
|
+
min(intent.confidence_score, strategy.confidence), 2
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return ReasoningResult(
|
|
205
|
+
intent=intent,
|
|
206
|
+
strategy=strategy,
|
|
207
|
+
reasoning_steps=reasoning_steps,
|
|
208
|
+
response_text=response_text,
|
|
209
|
+
execution_plan=execution_plan,
|
|
210
|
+
overall_confidence=overall_confidence,
|
|
211
|
+
retries_used=retries_used,
|
|
212
|
+
used_fallback=used_fallback,
|
|
213
|
+
metadata={
|
|
214
|
+
"context_items_used": len(context_strings),
|
|
215
|
+
"active_file": active_file,
|
|
216
|
+
"detected_entities_count": len(intent.detected_entities),
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Intent Analysis & Strategy Selection
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
async def analyze_intent(
|
|
225
|
+
self, request: str, context: list[str] | None = None
|
|
226
|
+
) -> IntentAnalysis:
|
|
227
|
+
"""Analyzes request to classify intent, risk level, and detected entities."""
|
|
228
|
+
if not request.strip():
|
|
229
|
+
return self._heuristic_intent_analysis("")
|
|
230
|
+
|
|
231
|
+
if self.provider and hasattr(self.provider, "chat"):
|
|
232
|
+
try:
|
|
233
|
+
# LLM-based intent extraction
|
|
234
|
+
intent_data = await self._llm_intent_classification(request, context)
|
|
235
|
+
if intent_data and intent_data.confidence_score >= self.confidence_threshold:
|
|
236
|
+
return intent_data
|
|
237
|
+
# Intentionally broad to isolate execution boundaries and prevent crashes.
|
|
238
|
+
except Exception as err: # noqa: BLE001
|
|
239
|
+
logger.debug(f"LLM intent analysis exception: {err}")
|
|
240
|
+
|
|
241
|
+
# Deterministic fallback
|
|
242
|
+
return self._heuristic_intent_analysis(request)
|
|
243
|
+
|
|
244
|
+
async def select_strategy(
|
|
245
|
+
self, intent: IntentAnalysis, context: list[str] | None = None
|
|
246
|
+
) -> ExecutionStrategy:
|
|
247
|
+
"""Determines the optimal execution strategy based on intent, safety, and tools."""
|
|
248
|
+
# Risk assessment via SafetyManager
|
|
249
|
+
risk = intent.risk_level
|
|
250
|
+
selected_tools: list[str] = []
|
|
251
|
+
|
|
252
|
+
# Tool discovery via ToolRegistry
|
|
253
|
+
if self.tool_registry:
|
|
254
|
+
registered_tools = self.tool_registry.discover()
|
|
255
|
+
for tool in registered_tools:
|
|
256
|
+
# Simple keyword/matching heuristic for tool selection
|
|
257
|
+
if any(
|
|
258
|
+
entity.lower() in tool.name.lower() or tool.name.lower() in entity.lower()
|
|
259
|
+
for entity in intent.detected_entities
|
|
260
|
+
) or (intent.category == IntentCategory.TOOL_EXECUTION and tool.name):
|
|
261
|
+
selected_tools.append(tool.name)
|
|
262
|
+
|
|
263
|
+
# Strategy decision matrix
|
|
264
|
+
if intent.category == IntentCategory.CLARIFICATION_NEEDED:
|
|
265
|
+
strategy_type = ExecutionStrategyType.CLARIFICATION_REQUEST
|
|
266
|
+
requires_planning = False
|
|
267
|
+
requires_safety = False
|
|
268
|
+
elif risk == RiskLevel.HIGH:
|
|
269
|
+
strategy_type = ExecutionStrategyType.SAFE_USER_CONFIRMATION
|
|
270
|
+
requires_planning = True
|
|
271
|
+
requires_safety = True
|
|
272
|
+
elif intent.category == IntentCategory.FILE_EDIT or intent.category == IntentCategory.TOOL_EXECUTION:
|
|
273
|
+
strategy_type = ExecutionStrategyType.PLANNED_TOOL_EXECUTION
|
|
274
|
+
requires_planning = True
|
|
275
|
+
requires_safety = risk in (RiskLevel.MEDIUM, RiskLevel.HIGH)
|
|
276
|
+
elif intent.category == IntentCategory.REPOSITORY_SEARCH:
|
|
277
|
+
strategy_type = ExecutionStrategyType.PLANNED_TOOL_EXECUTION
|
|
278
|
+
requires_planning = False
|
|
279
|
+
requires_safety = False
|
|
280
|
+
else: # DIRECT_ANSWER
|
|
281
|
+
strategy_type = ExecutionStrategyType.DIRECT_RESPONSE
|
|
282
|
+
requires_planning = False
|
|
283
|
+
requires_safety = False
|
|
284
|
+
|
|
285
|
+
steps = self._build_strategy_steps(intent, strategy_type, risk, selected_tools)
|
|
286
|
+
|
|
287
|
+
return ExecutionStrategy(
|
|
288
|
+
strategy_type=strategy_type,
|
|
289
|
+
steps=steps,
|
|
290
|
+
estimated_risk=risk,
|
|
291
|
+
requires_planning=requires_planning,
|
|
292
|
+
requires_safety_approval=requires_safety,
|
|
293
|
+
selected_tools=selected_tools,
|
|
294
|
+
confidence=intent.confidence_score,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
# Internal Helpers, Fallbacks & Sanitization
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
async def _analyze_intent_with_fallback(
|
|
302
|
+
self, request: str, context: list[str]
|
|
303
|
+
) -> tuple[IntentAnalysis, int, bool]:
|
|
304
|
+
"""Runs intent analysis with retries and deterministic fallback."""
|
|
305
|
+
if not self.provider:
|
|
306
|
+
return self._heuristic_intent_analysis(request), 0, False
|
|
307
|
+
|
|
308
|
+
retries = 0
|
|
309
|
+
for attempt in range(self.max_retries):
|
|
310
|
+
try:
|
|
311
|
+
intent = await self._llm_intent_classification(request, context)
|
|
312
|
+
if intent and intent.confidence_score >= self.confidence_threshold:
|
|
313
|
+
return intent, attempt, False
|
|
314
|
+
# Intentionally broad to isolate execution boundaries and prevent crashes.
|
|
315
|
+
except Exception as err: # noqa: BLE001
|
|
316
|
+
logger.warning(f"LLM intent classification attempt {attempt + 1} failed: {err}")
|
|
317
|
+
retries += 1
|
|
318
|
+
|
|
319
|
+
fallback_intent = self._heuristic_intent_analysis(request)
|
|
320
|
+
return fallback_intent, retries, True
|
|
321
|
+
|
|
322
|
+
def _heuristic_intent_analysis(self, request: str) -> IntentAnalysis:
|
|
323
|
+
"""Deterministic heuristic analysis for rule-based fallback and instant parsing."""
|
|
324
|
+
req_lower = request.lower().strip()
|
|
325
|
+
if not req_lower:
|
|
326
|
+
return IntentAnalysis(
|
|
327
|
+
category=IntentCategory.CLARIFICATION_NEEDED,
|
|
328
|
+
summary="Empty user prompt provided.",
|
|
329
|
+
confidence_score=1.0,
|
|
330
|
+
risk_level=RiskLevel.LOW,
|
|
331
|
+
ambiguities=["No request content."],
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Detect files & symbols
|
|
335
|
+
entities = re.findall(r"[a-zA-Z0-9_./-]+\.[a-zA-Z0-9_]+", request)
|
|
336
|
+
|
|
337
|
+
# Risk assessment
|
|
338
|
+
risk = self.safety_manager.assess_risk(request)
|
|
339
|
+
|
|
340
|
+
# Heuristic rules
|
|
341
|
+
if any(kw in req_lower for kw in ["how much work", "what is", "explain", "describe", "why does", "tell me about"]):
|
|
342
|
+
category = IntentCategory.DIRECT_ANSWER
|
|
343
|
+
confidence = 0.85
|
|
344
|
+
elif any(kw in req_lower for kw in ["find", "search", "where is", "grep", "locate"]):
|
|
345
|
+
category = IntentCategory.REPOSITORY_SEARCH
|
|
346
|
+
confidence = 0.85
|
|
347
|
+
elif any(kw in req_lower for kw in ["fix", "edit", "update", "modify", "add", "change", "refactor", "implement", "write"]):
|
|
348
|
+
category = IntentCategory.FILE_EDIT
|
|
349
|
+
confidence = 0.80
|
|
350
|
+
elif any(kw in req_lower for kw in ["run", "execute", "test", "pytest", "build", "terminal", "command"]):
|
|
351
|
+
category = IntentCategory.TOOL_EXECUTION
|
|
352
|
+
confidence = 0.80
|
|
353
|
+
elif len(req_lower.split()) < 2:
|
|
354
|
+
category = IntentCategory.CLARIFICATION_NEEDED
|
|
355
|
+
confidence = 0.90
|
|
356
|
+
else:
|
|
357
|
+
category = IntentCategory.DIRECT_ANSWER
|
|
358
|
+
confidence = 0.70
|
|
359
|
+
|
|
360
|
+
return IntentAnalysis(
|
|
361
|
+
category=category,
|
|
362
|
+
summary=f"Parsed intent: {category.value}",
|
|
363
|
+
confidence_score=confidence,
|
|
364
|
+
detected_entities=list(dict.fromkeys(entities)),
|
|
365
|
+
risk_level=risk,
|
|
366
|
+
ambiguities=[],
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
async def _llm_intent_classification(
|
|
370
|
+
self, request: str, context: list[str] | None
|
|
371
|
+
) -> IntentAnalysis | None:
|
|
372
|
+
"""Queries LLM provider for structured intent classification."""
|
|
373
|
+
if not self.provider:
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
prompt = (
|
|
377
|
+
"Analyze the following user prompt for a software engineering assistant.\n"
|
|
378
|
+
"Output JSON with fields:\n"
|
|
379
|
+
' "category": one of ["direct_answer", "tool_execution", "file_edit", "repository_search", "clarification_needed"]\n'
|
|
380
|
+
' "summary": concise string summary of user intent\n'
|
|
381
|
+
' "confidence": float between 0.0 and 1.0\n'
|
|
382
|
+
' "entities": array of file paths or code symbols referenced\n'
|
|
383
|
+
' "ambiguities": array of missing or ambiguous parameters\n\n'
|
|
384
|
+
f"Prompt: {request}\n"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
messages = [{"role": "user", "content": prompt}]
|
|
388
|
+
if hasattr(self.provider, "_chat") and asyncio.iscoroutinefunction(self.provider._chat):
|
|
389
|
+
raw_response = await self.provider._chat(messages, temperature=0.1)
|
|
390
|
+
else:
|
|
391
|
+
res = await asyncio.to_thread(self.provider.chat, messages, 0.1)
|
|
392
|
+
raw_response = str(await res) if (asyncio.iscoroutine(res) or hasattr(res, "__await__")) else str(res)
|
|
393
|
+
|
|
394
|
+
# Parse JSON from LLM response
|
|
395
|
+
match = re.search(r"\{.*\}", raw_response, re.DOTALL)
|
|
396
|
+
if not match:
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
data = json.loads(match.group(0))
|
|
400
|
+
cat_str = data.get("category", "direct_answer")
|
|
401
|
+
category = next((c for c in IntentCategory if c.value == cat_str), IntentCategory.DIRECT_ANSWER)
|
|
402
|
+
risk = self.safety_manager.assess_risk(request)
|
|
403
|
+
|
|
404
|
+
return IntentAnalysis(
|
|
405
|
+
category=category,
|
|
406
|
+
summary=data.get("summary", "LLM Intent Analysis"),
|
|
407
|
+
confidence_score=float(data.get("confidence", 0.8)),
|
|
408
|
+
detected_entities=data.get("entities", []),
|
|
409
|
+
risk_level=risk,
|
|
410
|
+
ambiguities=data.get("ambiguities", []),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def _sanitize_reasoning_steps(
|
|
414
|
+
self, steps: Sequence[ReasoningStep | dict[str, Any]]
|
|
415
|
+
) -> list[ReasoningStep]:
|
|
416
|
+
"""Ensures reasoning steps are clean public summaries without raw CoT / scratchpads."""
|
|
417
|
+
sanitized: list[ReasoningStep] = []
|
|
418
|
+
for index, item in enumerate(steps, start=1):
|
|
419
|
+
if isinstance(item, ReasoningStep):
|
|
420
|
+
clean_rationale = self._strip_cot(item.rationale)
|
|
421
|
+
sanitized.append(
|
|
422
|
+
ReasoningStep(
|
|
423
|
+
step_number=index,
|
|
424
|
+
title=item.title,
|
|
425
|
+
action=item.action,
|
|
426
|
+
rationale=clean_rationale,
|
|
427
|
+
status=item.status,
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
elif isinstance(item, dict):
|
|
431
|
+
clean_rationale = self._strip_cot(str(item.get("rationale", "")))
|
|
432
|
+
sanitized.append(
|
|
433
|
+
ReasoningStep(
|
|
434
|
+
step_number=index,
|
|
435
|
+
title=str(item.get("title", f"Step {index}")),
|
|
436
|
+
action=str(item.get("action", "evaluate")),
|
|
437
|
+
rationale=clean_rationale,
|
|
438
|
+
status=str(item.get("status", "completed")),
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
return sanitized
|
|
442
|
+
|
|
443
|
+
@staticmethod
|
|
444
|
+
def _strip_cot(text: str) -> str:
|
|
445
|
+
"""Removes internal chain-of-thought blocks (<think>, [scratchpad], etc.)."""
|
|
446
|
+
cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
|
|
447
|
+
cleaned = re.sub(r"\[scratchpad\].*?\[/scratchpad\]", "", cleaned, flags=re.DOTALL)
|
|
448
|
+
return cleaned.strip()
|
|
449
|
+
|
|
450
|
+
def _build_strategy_steps(
|
|
451
|
+
self,
|
|
452
|
+
intent: IntentAnalysis,
|
|
453
|
+
strategy_type: ExecutionStrategyType,
|
|
454
|
+
risk: RiskLevel,
|
|
455
|
+
tools: list[str],
|
|
456
|
+
) -> list[ReasoningStep]:
|
|
457
|
+
"""Generates standard public reasoning steps based on strategy decision."""
|
|
458
|
+
steps: list[ReasoningStep] = [
|
|
459
|
+
ReasoningStep(
|
|
460
|
+
step_number=1,
|
|
461
|
+
title="Intent Analysis",
|
|
462
|
+
action="analyze_intent",
|
|
463
|
+
rationale=f"Assessed user request as '{intent.category.value}' with {risk.value} risk level.",
|
|
464
|
+
),
|
|
465
|
+
ReasoningStep(
|
|
466
|
+
step_number=2,
|
|
467
|
+
title="Safety & Context Check",
|
|
468
|
+
action="verify_safety",
|
|
469
|
+
rationale=f"Evaluated execution safety ({risk.value} risk) and gathered active context.",
|
|
470
|
+
),
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
if strategy_type == ExecutionStrategyType.CLARIFICATION_REQUEST:
|
|
474
|
+
steps.append(
|
|
475
|
+
ReasoningStep(
|
|
476
|
+
step_number=3,
|
|
477
|
+
title="Formulate Clarification Request",
|
|
478
|
+
action="request_clarification",
|
|
479
|
+
rationale="Identified ambiguity or underspecified requirements in prompt.",
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
elif strategy_type == ExecutionStrategyType.SAFE_USER_CONFIRMATION:
|
|
483
|
+
steps.append(
|
|
484
|
+
ReasoningStep(
|
|
485
|
+
step_number=3,
|
|
486
|
+
title="Prepare User Confirmation",
|
|
487
|
+
action="request_confirmation",
|
|
488
|
+
rationale=f"High-risk action detected ({risk.value}). Requires explicit user approval.",
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
elif strategy_type == ExecutionStrategyType.PLANNED_TOOL_EXECUTION:
|
|
492
|
+
steps.append(
|
|
493
|
+
ReasoningStep(
|
|
494
|
+
step_number=3,
|
|
495
|
+
title="Formulate Execution Plan",
|
|
496
|
+
action="generate_plan",
|
|
497
|
+
rationale=f"Constructed structured execution plan utilizing tools: {tools or ['default_tools']}.",
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
else: # DIRECT_RESPONSE
|
|
501
|
+
steps.append(
|
|
502
|
+
ReasoningStep(
|
|
503
|
+
step_number=3,
|
|
504
|
+
title="Generate Direct Response",
|
|
505
|
+
action="direct_answer",
|
|
506
|
+
rationale="Formulated direct textual answer from gathered context.",
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
return steps
|
|
511
|
+
|
|
512
|
+
def _format_clarification_prompt(self, request: str, intent: IntentAnalysis) -> str:
|
|
513
|
+
"""Builds user-facing clarification message."""
|
|
514
|
+
ambiguities = ", ".join(intent.ambiguities) if intent.ambiguities else "underspecified parameters"
|
|
515
|
+
return (
|
|
516
|
+
f"Could you please clarify your request ('{request}')? "
|
|
517
|
+
f"Additional details regarding {ambiguities} will help me provide an accurate solution."
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
async def _generate_direct_response(
|
|
521
|
+
self, request: str, context: list[str]
|
|
522
|
+
) -> str:
|
|
523
|
+
"""Generates direct response via LLM provider."""
|
|
524
|
+
if not self.provider:
|
|
525
|
+
return f"Processed request: {request}"
|
|
526
|
+
|
|
527
|
+
prompt = "Answer the request based on the context:\n\nContext:\n" + "\n".join(context) + f"\n\nRequest: {request}"
|
|
528
|
+
messages = [{"role": "user", "content": prompt}]
|
|
529
|
+
try:
|
|
530
|
+
if hasattr(self.provider, "_chat") and asyncio.iscoroutinefunction(self.provider._chat):
|
|
531
|
+
return await self.provider._chat(messages, temperature=0.2)
|
|
532
|
+
res = await asyncio.to_thread(self.provider.chat, messages, 0.2)
|
|
533
|
+
if asyncio.iscoroutine(res) or hasattr(res, "__await__"):
|
|
534
|
+
return str(await res)
|
|
535
|
+
return str(res)
|
|
536
|
+
# Intentionally broad to isolate execution boundaries and prevent crashes.
|
|
537
|
+
except Exception as err: # noqa: BLE001
|
|
538
|
+
logger.warning(f"Direct response generation failed: {err}")
|
|
539
|
+
return f"Direct response unavailable due to provider error ({err})."
|
|
540
|
+
|
|
541
|
+
def _empty_request_result(self) -> ReasoningResult:
|
|
542
|
+
"""Handles empty requests gracefully."""
|
|
543
|
+
intent = IntentAnalysis(
|
|
544
|
+
category=IntentCategory.CLARIFICATION_NEEDED,
|
|
545
|
+
summary="Empty user prompt.",
|
|
546
|
+
confidence_score=1.0,
|
|
547
|
+
risk_level=RiskLevel.LOW,
|
|
548
|
+
ambiguities=["No message provided."],
|
|
549
|
+
)
|
|
550
|
+
strategy = ExecutionStrategy(
|
|
551
|
+
strategy_type=ExecutionStrategyType.CLARIFICATION_REQUEST,
|
|
552
|
+
steps=[
|
|
553
|
+
ReasoningStep(
|
|
554
|
+
step_number=1,
|
|
555
|
+
title="Empty Request",
|
|
556
|
+
action="clarify",
|
|
557
|
+
rationale="User submitted empty text prompt.",
|
|
558
|
+
)
|
|
559
|
+
],
|
|
560
|
+
estimated_risk=RiskLevel.LOW,
|
|
561
|
+
requires_planning=False,
|
|
562
|
+
requires_safety_approval=False,
|
|
563
|
+
)
|
|
564
|
+
return ReasoningResult(
|
|
565
|
+
intent=intent,
|
|
566
|
+
strategy=strategy,
|
|
567
|
+
reasoning_steps=strategy.steps,
|
|
568
|
+
response_text="Please provide a message or question to process.",
|
|
569
|
+
overall_confidence=1.0,
|
|
570
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ASTImpactAnalyzer:
|
|
8
|
+
"""Parses Python files to construct symbol cross-references and calculate refactoring impact."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, workspace: Path) -> None:
|
|
11
|
+
self.workspace = workspace.resolve()
|
|
12
|
+
self._symbol_map: dict[str, set[str]] = {}
|
|
13
|
+
self._analyzed = False
|
|
14
|
+
|
|
15
|
+
def build_symbol_map(self) -> None:
|
|
16
|
+
self._symbol_map.clear()
|
|
17
|
+
for path in self.workspace.rglob("*.py"):
|
|
18
|
+
# Skip ignored directories
|
|
19
|
+
if any(part in {".git", ".agent", ".venv", "venv", "__pycache__", ".pytest_cache"} for part in path.parts):
|
|
20
|
+
continue
|
|
21
|
+
relative_path = path.relative_to(self.workspace).as_posix()
|
|
22
|
+
try:
|
|
23
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
24
|
+
tree = ast.parse(content)
|
|
25
|
+
for node in ast.walk(tree):
|
|
26
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
27
|
+
self._add_symbol(node.name, relative_path)
|
|
28
|
+
elif isinstance(node, ast.Name):
|
|
29
|
+
self._add_symbol(node.id, relative_path)
|
|
30
|
+
elif isinstance(node, ast.Attribute):
|
|
31
|
+
self._add_symbol(node.attr, relative_path)
|
|
32
|
+
except SyntaxError:
|
|
33
|
+
continue
|
|
34
|
+
self._analyzed = True
|
|
35
|
+
|
|
36
|
+
def get_affected_files(self, symbol_name: str) -> list[str]:
|
|
37
|
+
if not self._analyzed:
|
|
38
|
+
self.build_symbol_map()
|
|
39
|
+
return sorted(self._symbol_map.get(symbol_name, set()))
|
|
40
|
+
|
|
41
|
+
def _add_symbol(self, symbol: str, file_path: str) -> None:
|
|
42
|
+
if symbol not in self._symbol_map:
|
|
43
|
+
self._symbol_map[symbol] = set()
|
|
44
|
+
self._symbol_map[symbol].add(file_path)
|