@jaguilar87/gaia-ops 1.0.0
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.
- package/CHANGELOG.md +315 -0
- package/CLAUDE.md +154 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/agents/aws-troubleshooter.md +50 -0
- package/agents/claude-architect.md +821 -0
- package/agents/devops-developer.md +92 -0
- package/agents/gcp-troubleshooter.md +50 -0
- package/agents/gitops-operator.md +360 -0
- package/agents/terraform-architect.md +289 -0
- package/bin/gaia-init.js +620 -0
- package/commands/architect.md +97 -0
- package/commands/restore-session.md +87 -0
- package/commands/save-session.md +88 -0
- package/commands/session-status.md +61 -0
- package/commands/speckit.add-task.md +144 -0
- package/commands/speckit.analyze-task.md +65 -0
- package/commands/speckit.implement.md +96 -0
- package/commands/speckit.init.md +237 -0
- package/commands/speckit.plan.md +88 -0
- package/commands/speckit.specify.md +161 -0
- package/commands/speckit.tasks.md +188 -0
- package/config/AGENTS.md +162 -0
- package/config/agent-catalog.md +604 -0
- package/config/context-contracts.md +682 -0
- package/config/git-standards.md +674 -0
- package/config/git_standards.json +69 -0
- package/config/orchestration-workflow.md +735 -0
- package/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/hooks/__pycache__/pre_kubectl_security.cpython-312.pyc +0 -0
- package/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/hooks/__pycache__/subagent_stop.cpython-312.pyc +0 -0
- package/hooks/post_tool_use.py +463 -0
- package/hooks/pre_kubectl_security.py +205 -0
- package/hooks/pre_tool_use.py +530 -0
- package/hooks/session_start.py +315 -0
- package/hooks/subagent_stop.py +549 -0
- package/index.js +92 -0
- package/package.json +59 -0
- package/speckit/README.en.md +648 -0
- package/speckit/README.md +353 -0
- package/speckit/governance.md +169 -0
- package/speckit/scripts/check-prerequisites.sh +194 -0
- package/speckit/scripts/common.sh +126 -0
- package/speckit/scripts/create-new-feature.sh +131 -0
- package/speckit/scripts/init.sh +42 -0
- package/speckit/scripts/setup-plan.sh +95 -0
- package/speckit/scripts/update-agent-context.sh +718 -0
- package/speckit/templates/adr-template.md +118 -0
- package/speckit/templates/agent-file-template.md +23 -0
- package/speckit/templates/plan-template.md +233 -0
- package/speckit/templates/spec-template.md +116 -0
- package/speckit/templates/tasks-template-bkp.md +136 -0
- package/speckit/templates/tasks-template.md +345 -0
- package/templates/CLAUDE.template.md +170 -0
- package/templates/code-examples/approval_gate_workflow.py +141 -0
- package/templates/code-examples/clarification_workflow.py +94 -0
- package/templates/code-examples/commit_validation.py +86 -0
- package/templates/project-context.template.json +126 -0
- package/templates/settings.template.json +307 -0
- package/tools/__pycache__/agent_router.cpython-312.pyc +0 -0
- package/tools/__pycache__/approval_gate.cpython-312.pyc +0 -0
- package/tools/__pycache__/clarify_engine.cpython-312.pyc +0 -0
- package/tools/__pycache__/clarify_patterns.cpython-312.pyc +0 -0
- package/tools/__pycache__/commit_validator.cpython-312.pyc +0 -0
- package/tools/__pycache__/context_section_reader.cpython-312.pyc +0 -0
- package/tools/__pycache__/routing_dashboard.cpython-312.pyc +0 -0
- package/tools/__pycache__/routing_feedback.cpython-312.pyc +0 -0
- package/tools/__pycache__/semantic_matcher.cpython-312.pyc +0 -0
- package/tools/__pycache__/task_manager.cpython-312.pyc +0 -0
- package/tools/agent_capabilities.json +231 -0
- package/tools/agent_invoker_helper.py +239 -0
- package/tools/agent_router.py +730 -0
- package/tools/approval_gate.py +318 -0
- package/tools/clarify_engine.py +511 -0
- package/tools/clarify_patterns.py +356 -0
- package/tools/commit_validator.py +338 -0
- package/tools/context_provider.py +181 -0
- package/tools/context_section_reader.py +301 -0
- package/tools/demo_clarify.py +104 -0
- package/tools/generate_embeddings.py +168 -0
- package/tools/quicktriage_aws_troubleshooter.sh +45 -0
- package/tools/quicktriage_devops_developer.sh +38 -0
- package/tools/quicktriage_gcp_troubleshooter.sh +51 -0
- package/tools/quicktriage_gitops_operator.sh +47 -0
- package/tools/quicktriage_terraform_architect.sh +40 -0
- package/tools/semantic_matcher.py +222 -0
- package/tools/task_manager.py +547 -0
- package/tools/task_manager_README.md +395 -0
- package/tools/task_manager_example.py +215 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Intelligent Clarification Engine
|
|
3
|
+
|
|
4
|
+
Detects ambiguity in user prompts and asks targeted questions
|
|
5
|
+
before agent routing. Follows the approval_gate.py pattern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from typing import Dict, Any, List, Optional
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from difflib import get_close_matches
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ClarificationEngine:
|
|
16
|
+
"""
|
|
17
|
+
Analyzes user prompts for ambiguity and generates intelligent
|
|
18
|
+
clarification questions using project-context.json.
|
|
19
|
+
|
|
20
|
+
Design Pattern: Modeled after approval_gate.py
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, project_context_path: str = ".claude/project-context.json"):
|
|
24
|
+
self.project_context_path = project_context_path
|
|
25
|
+
self.project_context = self._load_project_context()
|
|
26
|
+
self.clarification_log_path = ".claude/logs/clarifications.jsonl"
|
|
27
|
+
self.config = self._load_config()
|
|
28
|
+
self._ensure_log_directory()
|
|
29
|
+
|
|
30
|
+
# ========================================================================
|
|
31
|
+
# PUBLIC API (Orchestrator Entry Points)
|
|
32
|
+
# ========================================================================
|
|
33
|
+
|
|
34
|
+
def detect_ambiguity(
|
|
35
|
+
self,
|
|
36
|
+
user_prompt: str,
|
|
37
|
+
command_context: Optional[Dict[str, Any]] = None
|
|
38
|
+
) -> Dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Analyze user prompt for ambiguity.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
user_prompt: Original user request
|
|
44
|
+
command_context: Optional context (e.g., Spec-Kit command type)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
{
|
|
48
|
+
"needs_clarification": bool,
|
|
49
|
+
"ambiguity_score": int (0-100),
|
|
50
|
+
"ambiguity_points": List[Dict], # What's ambiguous
|
|
51
|
+
"suggested_questions": List[str] # What to ask
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
# Import patterns here to avoid circular dependency
|
|
55
|
+
from clarify_patterns import detect_all_ambiguities
|
|
56
|
+
|
|
57
|
+
# Step 1: Detect all ambiguities using patterns
|
|
58
|
+
ambiguities = detect_all_ambiguities(user_prompt, self.project_context)
|
|
59
|
+
|
|
60
|
+
# Step 2: Filter by command context (if specified)
|
|
61
|
+
if command_context and "command" in command_context:
|
|
62
|
+
command_name = command_context["command"]
|
|
63
|
+
if command_name in self.config["command_rules"]:
|
|
64
|
+
command_rules = self.config["command_rules"][command_name]
|
|
65
|
+
if not command_rules.get("enabled", True):
|
|
66
|
+
# Clarification disabled for this command
|
|
67
|
+
return {
|
|
68
|
+
"needs_clarification": False,
|
|
69
|
+
"ambiguity_score": 0,
|
|
70
|
+
"ambiguity_points": [],
|
|
71
|
+
"suggested_questions": []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Filter by allowed patterns
|
|
75
|
+
allowed_patterns = command_rules.get("patterns", [])
|
|
76
|
+
if allowed_patterns:
|
|
77
|
+
ambiguities = [
|
|
78
|
+
amb for amb in ambiguities
|
|
79
|
+
if amb["pattern"] in allowed_patterns
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Step 3: Calculate overall ambiguity score
|
|
83
|
+
ambiguity_score = self._calculate_ambiguity_score(ambiguities)
|
|
84
|
+
|
|
85
|
+
# Step 4: Get threshold from config
|
|
86
|
+
threshold = self.config["global_settings"]["ambiguity_threshold"]
|
|
87
|
+
|
|
88
|
+
# Step 5: Determine if clarification is needed
|
|
89
|
+
needs_clarification = ambiguity_score > threshold
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"needs_clarification": needs_clarification,
|
|
93
|
+
"ambiguity_score": ambiguity_score,
|
|
94
|
+
"ambiguity_points": ambiguities,
|
|
95
|
+
"suggested_questions": [a["suggested_question"] for a in ambiguities]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def generate_questions(
|
|
99
|
+
self,
|
|
100
|
+
ambiguity_analysis: Dict[str, Any],
|
|
101
|
+
max_questions: int = 5
|
|
102
|
+
) -> Dict[str, Any]:
|
|
103
|
+
"""
|
|
104
|
+
Generate AskUserQuestion configuration.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
ambiguity_analysis: Output from detect_ambiguity()
|
|
108
|
+
max_questions: Maximum number of questions (default 5)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
{
|
|
112
|
+
"summary": str, # Markdown summary for user
|
|
113
|
+
"question_config": Dict, # For AskUserQuestion tool
|
|
114
|
+
"clarification_context": Dict # For enrich_prompt()
|
|
115
|
+
}
|
|
116
|
+
"""
|
|
117
|
+
ambiguities = ambiguity_analysis["ambiguity_points"][:max_questions]
|
|
118
|
+
|
|
119
|
+
# Step 1: Generate summary
|
|
120
|
+
summary = self._generate_summary(ambiguities, ambiguity_analysis["ambiguity_score"])
|
|
121
|
+
|
|
122
|
+
# Step 2: Build questions for AskUserQuestion
|
|
123
|
+
questions = []
|
|
124
|
+
for i, ambiguity in enumerate(ambiguities):
|
|
125
|
+
question = self._build_question(ambiguity, i+1)
|
|
126
|
+
questions.append(question)
|
|
127
|
+
|
|
128
|
+
# Step 3: Build AskUserQuestion config
|
|
129
|
+
question_config = {
|
|
130
|
+
"questions": questions
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Step 4: Store context for enrich_prompt()
|
|
134
|
+
clarification_context = {
|
|
135
|
+
"ambiguity_analysis": ambiguity_analysis,
|
|
136
|
+
"questions_asked": len(questions),
|
|
137
|
+
"ambiguities": ambiguities
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"summary": summary,
|
|
142
|
+
"question_config": question_config,
|
|
143
|
+
"clarification_context": clarification_context
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def enrich_prompt(
|
|
147
|
+
self,
|
|
148
|
+
original_prompt: str,
|
|
149
|
+
user_responses: Dict[str, Any],
|
|
150
|
+
clarification_context: Dict[str, Any]
|
|
151
|
+
) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Merge user responses into original prompt.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
Original: "Check the API"
|
|
157
|
+
Response: {"question_1": "📦 tcm-api"}
|
|
158
|
+
Enriched: "Check the API (tcm-api service in tcm-non-prod namespace)"
|
|
159
|
+
"""
|
|
160
|
+
enriched = original_prompt
|
|
161
|
+
|
|
162
|
+
# Extract responses
|
|
163
|
+
for question_id, answer in user_responses.items():
|
|
164
|
+
question_index = int(question_id.split("_")[1]) - 1
|
|
165
|
+
|
|
166
|
+
if question_index >= len(clarification_context["ambiguities"]):
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
ambiguity = clarification_context["ambiguities"][question_index]
|
|
170
|
+
|
|
171
|
+
# Clean answer (remove emoji if present)
|
|
172
|
+
clean_answer = self._clean_answer(answer)
|
|
173
|
+
|
|
174
|
+
# Validate answer against available options
|
|
175
|
+
validated_answer = self._validate_answer(clean_answer, ambiguity)
|
|
176
|
+
|
|
177
|
+
# Append clarification to prompt
|
|
178
|
+
clarification_note = f"\n\n[Clarification - {ambiguity['pattern']}]: {validated_answer}"
|
|
179
|
+
enriched += clarification_note
|
|
180
|
+
|
|
181
|
+
return enriched
|
|
182
|
+
|
|
183
|
+
def log_clarification(
|
|
184
|
+
self,
|
|
185
|
+
original_prompt: str,
|
|
186
|
+
enriched_prompt: str,
|
|
187
|
+
ambiguity_analysis: Dict[str, Any],
|
|
188
|
+
user_responses: Dict[str, Any]
|
|
189
|
+
):
|
|
190
|
+
"""Log clarification decision for audit trail."""
|
|
191
|
+
log_entry = {
|
|
192
|
+
"timestamp": datetime.now().isoformat(),
|
|
193
|
+
"original_prompt": original_prompt,
|
|
194
|
+
"enriched_prompt": enriched_prompt,
|
|
195
|
+
"ambiguity_score": ambiguity_analysis["ambiguity_score"],
|
|
196
|
+
"questions_asked": len(ambiguity_analysis["ambiguity_points"]),
|
|
197
|
+
"patterns_detected": [a["pattern"] for a in ambiguity_analysis["ambiguity_points"]],
|
|
198
|
+
"user_responses": user_responses
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
with open(self.clarification_log_path, "a") as f:
|
|
202
|
+
f.write(json.dumps(log_entry) + "\n")
|
|
203
|
+
|
|
204
|
+
# ========================================================================
|
|
205
|
+
# PRIVATE METHODS (Internal Logic)
|
|
206
|
+
# ========================================================================
|
|
207
|
+
|
|
208
|
+
def _load_project_context(self) -> Dict[str, Any]:
|
|
209
|
+
"""Load project-context.json."""
|
|
210
|
+
if not os.path.exists(self.project_context_path):
|
|
211
|
+
return {"sections": {}}
|
|
212
|
+
|
|
213
|
+
with open(self.project_context_path, "r") as f:
|
|
214
|
+
return json.load(f)
|
|
215
|
+
|
|
216
|
+
def _load_config(self) -> Dict[str, Any]:
|
|
217
|
+
"""Load clarification_rules.json."""
|
|
218
|
+
config_path = ".claude/config/clarification_rules.json"
|
|
219
|
+
if not os.path.exists(config_path):
|
|
220
|
+
# Return default config
|
|
221
|
+
return {
|
|
222
|
+
"global_settings": {
|
|
223
|
+
"enabled": True,
|
|
224
|
+
"ambiguity_threshold": 30,
|
|
225
|
+
"max_questions_per_cycle": 5
|
|
226
|
+
},
|
|
227
|
+
"command_rules": {},
|
|
228
|
+
"question_templates": {},
|
|
229
|
+
"emoji_map": {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
with open(config_path, "r") as f:
|
|
233
|
+
return json.load(f)
|
|
234
|
+
|
|
235
|
+
def _ensure_log_directory(self):
|
|
236
|
+
"""Ensure logs directory exists."""
|
|
237
|
+
os.makedirs(os.path.dirname(self.clarification_log_path), exist_ok=True)
|
|
238
|
+
|
|
239
|
+
def _calculate_ambiguity_score(self, ambiguities: List[Dict]) -> int:
|
|
240
|
+
"""Calculate overall ambiguity score (0-100)."""
|
|
241
|
+
if not ambiguities:
|
|
242
|
+
return 0
|
|
243
|
+
# Weighted average of top 3 ambiguities
|
|
244
|
+
top_weights = [a["weight"] for a in ambiguities[:3]]
|
|
245
|
+
return int(sum(top_weights) / len(top_weights))
|
|
246
|
+
|
|
247
|
+
def _generate_summary(self, ambiguities: List[Dict], ambiguity_score: int) -> str:
|
|
248
|
+
"""Generate human-readable summary of ambiguities."""
|
|
249
|
+
lines = []
|
|
250
|
+
lines.append("=" * 60)
|
|
251
|
+
lines.append("🔍 CLARIFICACIÓN NECESARIA")
|
|
252
|
+
lines.append("=" * 60)
|
|
253
|
+
lines.append("")
|
|
254
|
+
lines.append(f"**Score de ambigüedad:** {ambiguity_score}/100")
|
|
255
|
+
lines.append("")
|
|
256
|
+
lines.append("He detectado las siguientes ambigüedades en tu solicitud:")
|
|
257
|
+
lines.append("")
|
|
258
|
+
|
|
259
|
+
for i, amb in enumerate(ambiguities):
|
|
260
|
+
lines.append(f"**{i+1}. {amb['ambiguity_reason']}**")
|
|
261
|
+
options_preview = amb['available_options'][:5]
|
|
262
|
+
if len(amb['available_options']) > 5:
|
|
263
|
+
options_preview_str = ", ".join(options_preview) + f" (y {len(amb['available_options']) - 5} más)"
|
|
264
|
+
else:
|
|
265
|
+
options_preview_str = ", ".join(options_preview)
|
|
266
|
+
lines.append(f" Opciones disponibles: {options_preview_str}")
|
|
267
|
+
lines.append("")
|
|
268
|
+
|
|
269
|
+
lines.append("Por favor, responde las siguientes preguntas para continuar:")
|
|
270
|
+
lines.append("=" * 60)
|
|
271
|
+
|
|
272
|
+
return "\n".join(lines)
|
|
273
|
+
|
|
274
|
+
def _build_question(self, ambiguity: Dict[str, Any], question_num: int) -> Dict[str, Any]:
|
|
275
|
+
"""
|
|
276
|
+
Build question with 3-4 rich options.
|
|
277
|
+
|
|
278
|
+
This implements the optimized UX pattern:
|
|
279
|
+
- Emoji for visual scanning
|
|
280
|
+
- Short label (2-4 words)
|
|
281
|
+
- Rich description with metadata
|
|
282
|
+
- Max 4 options (3 specific + 1 catch-all)
|
|
283
|
+
"""
|
|
284
|
+
# Select appropriate emoji based on pattern
|
|
285
|
+
emoji_map = self.config.get("emoji_map", {})
|
|
286
|
+
pattern_name = ambiguity["pattern"].split("_")[0] # "service" from "service_ambiguity"
|
|
287
|
+
emoji = emoji_map.get(pattern_name, "❓")
|
|
288
|
+
|
|
289
|
+
# Build options with rich descriptions
|
|
290
|
+
options = []
|
|
291
|
+
available_options = ambiguity["available_options"][:3] # Max 3 specific options
|
|
292
|
+
|
|
293
|
+
for option_name in available_options:
|
|
294
|
+
# Fetch metadata from project_context
|
|
295
|
+
metadata = self._get_option_metadata(option_name, ambiguity)
|
|
296
|
+
|
|
297
|
+
options.append({
|
|
298
|
+
"label": f"{emoji} {option_name}",
|
|
299
|
+
"description": metadata
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
# Add catch-all 4th option if there are more than 3 options
|
|
303
|
+
if len(ambiguity["available_options"]) > 3:
|
|
304
|
+
total_count = len(ambiguity["available_options"])
|
|
305
|
+
resource_type = pattern_name # "service", "namespace", "resource"
|
|
306
|
+
|
|
307
|
+
options.append({
|
|
308
|
+
"label": f"{emoji_map.get('all', '🌐')} Todos los {resource_type}s",
|
|
309
|
+
"description": f"Aplicar a todos los recursos ({total_count} total)"
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
"question": ambiguity["suggested_question"],
|
|
314
|
+
"header": f"{emoji} Pregunta {question_num}",
|
|
315
|
+
"multiSelect": ambiguity.get("allow_multiple", False),
|
|
316
|
+
"options": options
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
def _get_option_metadata(self, option_name: str, ambiguity: Dict[str, Any]) -> str:
|
|
320
|
+
"""
|
|
321
|
+
Fetch rich metadata for option from project-context.json.
|
|
322
|
+
|
|
323
|
+
This generates the detailed description shown under each option.
|
|
324
|
+
"""
|
|
325
|
+
pattern = ambiguity["pattern"]
|
|
326
|
+
|
|
327
|
+
# Service metadata
|
|
328
|
+
if pattern == "service_ambiguity" and "services_metadata" in ambiguity:
|
|
329
|
+
svc = ambiguity["services_metadata"].get(option_name, {})
|
|
330
|
+
tech_stack = svc.get("tech_stack", "N/A")
|
|
331
|
+
namespace = svc.get("namespace", "N/A")
|
|
332
|
+
port = svc.get("port", "N/A")
|
|
333
|
+
status = svc.get("status", "unknown")
|
|
334
|
+
status_emoji = "✅" if status == "running" else "⏸️"
|
|
335
|
+
|
|
336
|
+
return f"{tech_stack} | Namespace: {namespace} | Puerto: {port} | Estado: {status_emoji} {status}"
|
|
337
|
+
|
|
338
|
+
# Namespace metadata
|
|
339
|
+
elif pattern == "namespace_ambiguity" and "namespace_metadata" in ambiguity:
|
|
340
|
+
ns_meta = ambiguity["namespace_metadata"].get(option_name, {})
|
|
341
|
+
services = ns_meta.get("services", [])
|
|
342
|
+
service_count = ns_meta.get("service_count", 0)
|
|
343
|
+
services_str = ", ".join(services[:3])
|
|
344
|
+
if len(services) > 3:
|
|
345
|
+
services_str += f" (y {len(services) - 3} más)"
|
|
346
|
+
|
|
347
|
+
return f"Servicios: {services_str} | Total: {service_count} servicios"
|
|
348
|
+
|
|
349
|
+
# Environment metadata
|
|
350
|
+
elif pattern == "environment_ambiguity":
|
|
351
|
+
current_env = ambiguity.get("current_environment", "unknown")
|
|
352
|
+
if "Continuar" in option_name:
|
|
353
|
+
return f"Proceder con el entorno actual ({current_env}). Operación segura."
|
|
354
|
+
elif "Detener" in option_name:
|
|
355
|
+
return f"Cancelar para configurar el proyecto correctamente primero"
|
|
356
|
+
elif "Forzar" in option_name:
|
|
357
|
+
return f"⚠️ Proceder en producción AUNQUE el contexto diga {current_env}. Solo si estás seguro."
|
|
358
|
+
|
|
359
|
+
# Resource metadata
|
|
360
|
+
elif pattern == "resource_ambiguity" and "resource_metadata" in ambiguity:
|
|
361
|
+
res_meta = ambiguity["resource_metadata"].get(option_name, {})
|
|
362
|
+
res_type = res_meta.get("type", "N/A")
|
|
363
|
+
status = res_meta.get("status", "unknown")
|
|
364
|
+
tier = res_meta.get("tier", "N/A")
|
|
365
|
+
namespace = res_meta.get("namespace", "")
|
|
366
|
+
|
|
367
|
+
parts = [res_type]
|
|
368
|
+
if tier != "N/A":
|
|
369
|
+
parts.append(f"Tier: {tier}")
|
|
370
|
+
if namespace:
|
|
371
|
+
parts.append(f"Namespace: {namespace}")
|
|
372
|
+
parts.append(f"Estado: {status}")
|
|
373
|
+
|
|
374
|
+
return " | ".join(parts)
|
|
375
|
+
|
|
376
|
+
# Default fallback
|
|
377
|
+
return f"Seleccionar: {option_name}"
|
|
378
|
+
|
|
379
|
+
def _clean_answer(self, answer: str) -> str:
|
|
380
|
+
"""Remove emoji and extra formatting from user's answer."""
|
|
381
|
+
# Remove common emoji
|
|
382
|
+
emoji_list = ["📦", "🎯", "🔧", "⚠️", "🌐", "✅", "❌", "🗄️", "🔴"]
|
|
383
|
+
cleaned = answer
|
|
384
|
+
for emoji in emoji_list:
|
|
385
|
+
cleaned = cleaned.replace(emoji, "")
|
|
386
|
+
return cleaned.strip()
|
|
387
|
+
|
|
388
|
+
def _validate_answer(self, answer: str, ambiguity: Dict[str, Any]) -> str:
|
|
389
|
+
"""
|
|
390
|
+
Validate user's answer against available options.
|
|
391
|
+
|
|
392
|
+
Implements fuzzy matching to handle variations like:
|
|
393
|
+
- "tcm api" → "tcm-api"
|
|
394
|
+
- "todos" → "Todos los servicios"
|
|
395
|
+
"""
|
|
396
|
+
available_options = ambiguity["available_options"]
|
|
397
|
+
|
|
398
|
+
# Exact match (after cleaning)
|
|
399
|
+
if answer in available_options:
|
|
400
|
+
return answer
|
|
401
|
+
|
|
402
|
+
# Fuzzy match
|
|
403
|
+
matches = get_close_matches(answer.lower(),
|
|
404
|
+
[opt.lower() for opt in available_options],
|
|
405
|
+
n=1, cutoff=0.6)
|
|
406
|
+
|
|
407
|
+
if matches:
|
|
408
|
+
# Find original option (with proper casing)
|
|
409
|
+
for opt in available_options:
|
|
410
|
+
if opt.lower() == matches[0]:
|
|
411
|
+
return opt
|
|
412
|
+
|
|
413
|
+
# Check for "all" / "todos" keywords
|
|
414
|
+
if any(keyword in answer.lower() for keyword in ["todos", "all", "ambos", "both"]):
|
|
415
|
+
# User wants all options
|
|
416
|
+
return f"Todos ({', '.join(available_options)})"
|
|
417
|
+
|
|
418
|
+
# No match - return original answer
|
|
419
|
+
return answer
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ============================================================================
|
|
423
|
+
# CONVENIENCE FUNCTIONS (Orchestrator API)
|
|
424
|
+
# ============================================================================
|
|
425
|
+
|
|
426
|
+
def request_clarification(
|
|
427
|
+
user_prompt: str,
|
|
428
|
+
command_context: Optional[Dict[str, Any]] = None
|
|
429
|
+
) -> Dict[str, Any]:
|
|
430
|
+
"""
|
|
431
|
+
Main function to request clarification from user.
|
|
432
|
+
|
|
433
|
+
This should be called by the orchestrator in Phase 0.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
user_prompt: Original user request
|
|
437
|
+
command_context: Optional context (e.g., {"command": "speckit.init"})
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
{
|
|
441
|
+
"needs_clarification": bool,
|
|
442
|
+
"summary": str, # Markdown summary (if clarification needed)
|
|
443
|
+
"question_config": Dict, # For AskUserQuestion tool
|
|
444
|
+
"engine_instance": ClarificationEngine, # For process_clarification()
|
|
445
|
+
"clarification_context": Dict # For enrich_prompt()
|
|
446
|
+
}
|
|
447
|
+
"""
|
|
448
|
+
engine = ClarificationEngine()
|
|
449
|
+
ambiguity = engine.detect_ambiguity(user_prompt, command_context)
|
|
450
|
+
|
|
451
|
+
if not ambiguity["needs_clarification"]:
|
|
452
|
+
return {
|
|
453
|
+
"needs_clarification": False,
|
|
454
|
+
"enriched_prompt": user_prompt # No changes
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
questions = engine.generate_questions(ambiguity)
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
"needs_clarification": True,
|
|
461
|
+
"summary": questions["summary"],
|
|
462
|
+
"question_config": questions["question_config"],
|
|
463
|
+
"engine_instance": engine,
|
|
464
|
+
"clarification_context": questions["clarification_context"],
|
|
465
|
+
"original_prompt": user_prompt
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def process_clarification(
|
|
470
|
+
engine_instance: ClarificationEngine,
|
|
471
|
+
original_prompt: str,
|
|
472
|
+
user_responses: Dict[str, Any],
|
|
473
|
+
clarification_context: Dict[str, Any]
|
|
474
|
+
) -> Dict[str, Any]:
|
|
475
|
+
"""
|
|
476
|
+
Process user's clarification responses.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
engine_instance: The ClarificationEngine instance from request_clarification
|
|
480
|
+
original_prompt: Original user request
|
|
481
|
+
user_responses: Response from AskUserQuestion
|
|
482
|
+
clarification_context: Context from request_clarification
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
{
|
|
486
|
+
"enriched_prompt": str, # Enriched prompt for agent_router.py
|
|
487
|
+
"clarification_log": Dict # Log entry
|
|
488
|
+
}
|
|
489
|
+
"""
|
|
490
|
+
enriched_prompt = engine_instance.enrich_prompt(
|
|
491
|
+
original_prompt,
|
|
492
|
+
user_responses,
|
|
493
|
+
clarification_context
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Log the clarification
|
|
497
|
+
engine_instance.log_clarification(
|
|
498
|
+
original_prompt,
|
|
499
|
+
enriched_prompt,
|
|
500
|
+
clarification_context["ambiguity_analysis"],
|
|
501
|
+
user_responses
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
"enriched_prompt": enriched_prompt,
|
|
506
|
+
"clarification_log": {
|
|
507
|
+
"timestamp": datetime.now().isoformat(),
|
|
508
|
+
"original_prompt": original_prompt,
|
|
509
|
+
"enriched_prompt": enriched_prompt
|
|
510
|
+
}
|
|
511
|
+
}
|