nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM-Based Task Classifier
|
|
3
|
+
Uses a small, fast LLM call to intelligently classify user requests
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskCategory(Enum):
|
|
12
|
+
"""Categories for task classification"""
|
|
13
|
+
CODE_GENERATION = "code_generation" # Write new code
|
|
14
|
+
CODE_EXPLANATION = "code_explanation" # Explain existing code
|
|
15
|
+
CODE_REFACTORING = "code_refactoring" # Improve/refactor code
|
|
16
|
+
CODE_DEBUGGING = "code_debugging" # Fix bugs/errors
|
|
17
|
+
FILE_OPERATIONS = "file_operations" # Read/write/modify files
|
|
18
|
+
SHELL_COMMANDS = "shell_commands" # Execute terminal commands
|
|
19
|
+
GIT_OPERATIONS = "git_operations" # Git-related tasks
|
|
20
|
+
DOCKER_OPERATIONS = "docker_operations" # Docker-related tasks
|
|
21
|
+
PROJECT_SETUP = "project_setup" # Create/scaffold projects
|
|
22
|
+
DOCUMENTATION = "documentation" # Write docs, comments
|
|
23
|
+
TESTING = "testing" # Generate/run tests
|
|
24
|
+
ARCHITECTURE = "architecture" # Design, planning
|
|
25
|
+
GENERAL_QA = "general_qa" # General questions
|
|
26
|
+
QUICK_ANSWER = "quick_answer" # Simple, fast responses
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TaskComplexity(Enum):
|
|
30
|
+
"""Complexity levels for tasks"""
|
|
31
|
+
TRIVIAL = "trivial" # Single-step, instant
|
|
32
|
+
SIMPLE = "simple" # Few steps, quick
|
|
33
|
+
MODERATE = "moderate" # Multiple steps
|
|
34
|
+
COMPLEX = "complex" # Many steps, planning needed
|
|
35
|
+
EXPERT = "expert" # Requires deep analysis
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ClassificationResult:
|
|
39
|
+
"""Result of task classification"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
category: TaskCategory,
|
|
44
|
+
complexity: TaskComplexity,
|
|
45
|
+
confidence: float,
|
|
46
|
+
suggested_model: str,
|
|
47
|
+
requires_tools: bool,
|
|
48
|
+
estimated_steps: int,
|
|
49
|
+
reasoning: str
|
|
50
|
+
):
|
|
51
|
+
self.category = category
|
|
52
|
+
self.complexity = complexity
|
|
53
|
+
self.confidence = confidence
|
|
54
|
+
self.suggested_model = suggested_model
|
|
55
|
+
self.requires_tools = requires_tools
|
|
56
|
+
self.estimated_steps = estimated_steps
|
|
57
|
+
self.reasoning = reasoning
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
60
|
+
return {
|
|
61
|
+
"category": self.category.value,
|
|
62
|
+
"complexity": self.complexity.value,
|
|
63
|
+
"confidence": self.confidence,
|
|
64
|
+
"suggested_model": self.suggested_model,
|
|
65
|
+
"requires_tools": self.requires_tools,
|
|
66
|
+
"estimated_steps": self.estimated_steps,
|
|
67
|
+
"reasoning": self.reasoning
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SmartTaskClassifier:
|
|
72
|
+
"""LLM-powered task classifier with fallback to keyword matching"""
|
|
73
|
+
|
|
74
|
+
# Fast classification prompt
|
|
75
|
+
CLASSIFICATION_PROMPT = """Classify this task. Respond with ONLY a JSON object, no other text.
|
|
76
|
+
|
|
77
|
+
Task: {task}
|
|
78
|
+
|
|
79
|
+
JSON format:
|
|
80
|
+
{{
|
|
81
|
+
"category": "code_generation|code_explanation|code_refactoring|code_debugging|file_operations|shell_commands|git_operations|docker_operations|project_setup|documentation|testing|architecture|general_qa|quick_answer",
|
|
82
|
+
"complexity": "trivial|simple|moderate|complex|expert",
|
|
83
|
+
"confidence": 0.0-1.0,
|
|
84
|
+
"requires_tools": true|false,
|
|
85
|
+
"steps": 1-10,
|
|
86
|
+
"reason": "brief explanation"
|
|
87
|
+
}}"""
|
|
88
|
+
|
|
89
|
+
# Keyword patterns for fast fallback classification
|
|
90
|
+
PATTERNS = {
|
|
91
|
+
TaskCategory.CODE_GENERATION: [
|
|
92
|
+
r"\b(write|create|generate|implement|build|make)\b.*\b(function|class|code|script|program|api|module)\b",
|
|
93
|
+
r"\b(code|function|class|script)\b.*\b(for|that|to|which)\b"
|
|
94
|
+
],
|
|
95
|
+
TaskCategory.CODE_EXPLANATION: [
|
|
96
|
+
r"\b(explain|what does|how does|understand|clarify)\b.*\b(code|function|this|it)\b",
|
|
97
|
+
r"\bwhat\s+is\b.*\b(doing|for|purpose)\b"
|
|
98
|
+
],
|
|
99
|
+
TaskCategory.CODE_REFACTORING: [
|
|
100
|
+
r"\b(refactor|improve|optimize|clean up|simplify|rewrite)\b",
|
|
101
|
+
r"\bmake\b.*\b(better|cleaner|faster|readable)\b"
|
|
102
|
+
],
|
|
103
|
+
TaskCategory.CODE_DEBUGGING: [
|
|
104
|
+
r"\b(fix|debug|error|bug|issue|problem|broken|not working)\b",
|
|
105
|
+
r"\bwhy\b.*\b(fail|error|crash|wrong)\b"
|
|
106
|
+
],
|
|
107
|
+
TaskCategory.FILE_OPERATIONS: [
|
|
108
|
+
r"\b(read|write|create|delete|modify|edit|save)\b.*\b(file|directory|folder)\b",
|
|
109
|
+
r"\bfile\s+(content|path|name)\b"
|
|
110
|
+
],
|
|
111
|
+
TaskCategory.SHELL_COMMANDS: [
|
|
112
|
+
r"\b(run|execute|command|terminal|shell|bash)\b",
|
|
113
|
+
r"\b(npm|pip|yarn|cargo|make|apt|brew)\b"
|
|
114
|
+
],
|
|
115
|
+
TaskCategory.GIT_OPERATIONS: [
|
|
116
|
+
r"\b(git|commit|push|pull|branch|merge|clone|diff|status)\b"
|
|
117
|
+
],
|
|
118
|
+
TaskCategory.DOCKER_OPERATIONS: [
|
|
119
|
+
r"\b(docker|container|image|compose|kubernetes|k8s)\b"
|
|
120
|
+
],
|
|
121
|
+
TaskCategory.PROJECT_SETUP: [
|
|
122
|
+
r"\b(setup|scaffold|initialize|bootstrap|create project|new project)\b",
|
|
123
|
+
r"\b(fastapi|django|flask|next\.?js|react|vue)\b.*\b(project|app)\b"
|
|
124
|
+
],
|
|
125
|
+
TaskCategory.DOCUMENTATION: [
|
|
126
|
+
r"\b(document|docstring|readme|comment|annotate)\b",
|
|
127
|
+
r"\badd\b.*\b(docs|documentation|comments)\b"
|
|
128
|
+
],
|
|
129
|
+
TaskCategory.TESTING: [
|
|
130
|
+
r"\b(test|unittest|pytest|jest|spec|coverage)\b",
|
|
131
|
+
r"\bgenerate\b.*\btests?\b"
|
|
132
|
+
],
|
|
133
|
+
TaskCategory.ARCHITECTURE: [
|
|
134
|
+
r"\b(design|architect|plan|structure|organize)\b",
|
|
135
|
+
r"\bhow\s+should\s+i\b"
|
|
136
|
+
],
|
|
137
|
+
TaskCategory.QUICK_ANSWER: [
|
|
138
|
+
r"^(what is|who is|when|where|how many|yes or no)\b",
|
|
139
|
+
r"^.{0,30}\?$" # Short questions
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Complexity indicators
|
|
144
|
+
COMPLEXITY_PATTERNS = {
|
|
145
|
+
TaskComplexity.TRIVIAL: [
|
|
146
|
+
r"^(what is|define|list)\b",
|
|
147
|
+
r"\bjust\b",
|
|
148
|
+
r"\bsimple\b"
|
|
149
|
+
],
|
|
150
|
+
TaskComplexity.SIMPLE: [
|
|
151
|
+
r"\b(quick|brief|short)\b",
|
|
152
|
+
r"\bone\b.*\b(function|file|class)\b"
|
|
153
|
+
],
|
|
154
|
+
TaskComplexity.MODERATE: [
|
|
155
|
+
r"\b(several|few|some)\b",
|
|
156
|
+
r"\band\b.*\band\b" # Multiple requirements
|
|
157
|
+
],
|
|
158
|
+
TaskComplexity.COMPLEX: [
|
|
159
|
+
r"\b(entire|whole|full|complete)\b.*\b(app|application|system)\b",
|
|
160
|
+
r"\brefactor\b.*\b(entire|whole|all)\b"
|
|
161
|
+
],
|
|
162
|
+
TaskComplexity.EXPERT: [
|
|
163
|
+
r"\b(migrate|architecture|redesign|scale)\b",
|
|
164
|
+
r"\bfrom scratch\b"
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Model recommendations by category and complexity
|
|
169
|
+
MODEL_RECOMMENDATIONS = {
|
|
170
|
+
(TaskCategory.QUICK_ANSWER, TaskComplexity.TRIVIAL): "fast",
|
|
171
|
+
(TaskCategory.QUICK_ANSWER, TaskComplexity.SIMPLE): "fast",
|
|
172
|
+
(TaskCategory.CODE_GENERATION, TaskComplexity.TRIVIAL): "fast",
|
|
173
|
+
(TaskCategory.CODE_GENERATION, TaskComplexity.SIMPLE): "coding",
|
|
174
|
+
(TaskCategory.CODE_GENERATION, TaskComplexity.MODERATE): "coding",
|
|
175
|
+
(TaskCategory.CODE_GENERATION, TaskComplexity.COMPLEX): "coding",
|
|
176
|
+
(TaskCategory.CODE_GENERATION, TaskComplexity.EXPERT): "reasoning",
|
|
177
|
+
(TaskCategory.ARCHITECTURE, TaskComplexity.MODERATE): "reasoning",
|
|
178
|
+
(TaskCategory.ARCHITECTURE, TaskComplexity.COMPLEX): "reasoning",
|
|
179
|
+
(TaskCategory.ARCHITECTURE, TaskComplexity.EXPERT): "reasoning",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
def __init__(self, llm_adapter=None):
|
|
183
|
+
"""Initialize classifier with optional LLM for smart classification
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
llm_adapter: LLMAdapter instance for LLM-based classification
|
|
187
|
+
"""
|
|
188
|
+
self.llm = llm_adapter
|
|
189
|
+
self._use_llm = llm_adapter is not None
|
|
190
|
+
|
|
191
|
+
def classify(
|
|
192
|
+
self,
|
|
193
|
+
task: str,
|
|
194
|
+
context: Optional[Dict[str, Any]] = None,
|
|
195
|
+
use_llm: bool = True
|
|
196
|
+
) -> ClassificationResult:
|
|
197
|
+
"""Classify a task using LLM or keyword matching
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
task: User's task/request
|
|
201
|
+
context: Additional context (file paths, history, etc.)
|
|
202
|
+
use_llm: Whether to attempt LLM classification
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
ClassificationResult with category, complexity, and recommendations
|
|
206
|
+
"""
|
|
207
|
+
# Try LLM classification first if available and enabled
|
|
208
|
+
if use_llm and self._use_llm and self.llm:
|
|
209
|
+
try:
|
|
210
|
+
result = self._classify_with_llm(task)
|
|
211
|
+
if result and result.confidence >= 0.7:
|
|
212
|
+
return result
|
|
213
|
+
except Exception:
|
|
214
|
+
pass # Fall back to keyword matching
|
|
215
|
+
|
|
216
|
+
# Fall back to keyword matching
|
|
217
|
+
return self._classify_with_keywords(task, context)
|
|
218
|
+
|
|
219
|
+
def _classify_with_llm(self, task: str) -> Optional[ClassificationResult]:
|
|
220
|
+
"""Classify task using LLM
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
task: User's task
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
ClassificationResult or None if classification fails
|
|
227
|
+
"""
|
|
228
|
+
from .llm_adapter import TaskType
|
|
229
|
+
|
|
230
|
+
prompt = self.CLASSIFICATION_PROMPT.format(task=task)
|
|
231
|
+
|
|
232
|
+
# Use fast model for classification
|
|
233
|
+
response = self.llm.complete(prompt, task_type=TaskType.FAST, max_tokens=200)
|
|
234
|
+
|
|
235
|
+
# Parse JSON response
|
|
236
|
+
try:
|
|
237
|
+
# Extract JSON from response
|
|
238
|
+
json_match = re.search(r'\{[^{}]+\}', response, re.DOTALL)
|
|
239
|
+
if not json_match:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
data = json.loads(json_match.group())
|
|
243
|
+
|
|
244
|
+
category = TaskCategory(data.get("category", "general_qa"))
|
|
245
|
+
complexity = TaskComplexity(data.get("complexity", "moderate"))
|
|
246
|
+
confidence = float(data.get("confidence", 0.8))
|
|
247
|
+
requires_tools = bool(data.get("requires_tools", False))
|
|
248
|
+
steps = int(data.get("steps", 1))
|
|
249
|
+
reason = data.get("reason", "LLM classification")
|
|
250
|
+
|
|
251
|
+
# Get model recommendation
|
|
252
|
+
suggested_model = self._get_model_recommendation(category, complexity)
|
|
253
|
+
|
|
254
|
+
return ClassificationResult(
|
|
255
|
+
category=category,
|
|
256
|
+
complexity=complexity,
|
|
257
|
+
confidence=confidence,
|
|
258
|
+
suggested_model=suggested_model,
|
|
259
|
+
requires_tools=requires_tools,
|
|
260
|
+
estimated_steps=steps,
|
|
261
|
+
reasoning=reason
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
except (json.JSONDecodeError, ValueError, KeyError):
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
def _classify_with_keywords(
|
|
268
|
+
self,
|
|
269
|
+
task: str,
|
|
270
|
+
context: Optional[Dict[str, Any]] = None
|
|
271
|
+
) -> ClassificationResult:
|
|
272
|
+
"""Classify task using keyword patterns
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
task: User's task
|
|
276
|
+
context: Additional context
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
ClassificationResult
|
|
280
|
+
"""
|
|
281
|
+
task_lower = task.lower()
|
|
282
|
+
|
|
283
|
+
# Find best matching category
|
|
284
|
+
best_category = TaskCategory.GENERAL_QA
|
|
285
|
+
best_score = 0
|
|
286
|
+
|
|
287
|
+
for category, patterns in self.PATTERNS.items():
|
|
288
|
+
score = 0
|
|
289
|
+
for pattern in patterns:
|
|
290
|
+
if re.search(pattern, task_lower, re.IGNORECASE):
|
|
291
|
+
score += 1
|
|
292
|
+
|
|
293
|
+
if score > best_score:
|
|
294
|
+
best_score = score
|
|
295
|
+
best_category = category
|
|
296
|
+
|
|
297
|
+
# Determine complexity
|
|
298
|
+
complexity = TaskComplexity.MODERATE
|
|
299
|
+
for comp_level, patterns in self.COMPLEXITY_PATTERNS.items():
|
|
300
|
+
for pattern in patterns:
|
|
301
|
+
if re.search(pattern, task_lower, re.IGNORECASE):
|
|
302
|
+
complexity = comp_level
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
# Estimate based on task length and keywords
|
|
306
|
+
word_count = len(task.split())
|
|
307
|
+
if word_count < 10:
|
|
308
|
+
complexity = TaskComplexity.SIMPLE
|
|
309
|
+
elif word_count > 50:
|
|
310
|
+
complexity = TaskComplexity.COMPLEX
|
|
311
|
+
|
|
312
|
+
# Determine if tools are required
|
|
313
|
+
requires_tools = best_category in [
|
|
314
|
+
TaskCategory.FILE_OPERATIONS,
|
|
315
|
+
TaskCategory.SHELL_COMMANDS,
|
|
316
|
+
TaskCategory.GIT_OPERATIONS,
|
|
317
|
+
TaskCategory.DOCKER_OPERATIONS,
|
|
318
|
+
TaskCategory.PROJECT_SETUP,
|
|
319
|
+
TaskCategory.TESTING
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
# Estimate steps
|
|
323
|
+
steps_map = {
|
|
324
|
+
TaskComplexity.TRIVIAL: 1,
|
|
325
|
+
TaskComplexity.SIMPLE: 2,
|
|
326
|
+
TaskComplexity.MODERATE: 4,
|
|
327
|
+
TaskComplexity.COMPLEX: 7,
|
|
328
|
+
TaskComplexity.EXPERT: 10
|
|
329
|
+
}
|
|
330
|
+
estimated_steps = steps_map.get(complexity, 3)
|
|
331
|
+
|
|
332
|
+
# Get model recommendation
|
|
333
|
+
suggested_model = self._get_model_recommendation(best_category, complexity)
|
|
334
|
+
|
|
335
|
+
# Calculate confidence based on match score
|
|
336
|
+
confidence = min(0.5 + (best_score * 0.15), 0.95)
|
|
337
|
+
|
|
338
|
+
return ClassificationResult(
|
|
339
|
+
category=best_category,
|
|
340
|
+
complexity=complexity,
|
|
341
|
+
confidence=confidence,
|
|
342
|
+
suggested_model=suggested_model,
|
|
343
|
+
requires_tools=requires_tools,
|
|
344
|
+
estimated_steps=estimated_steps,
|
|
345
|
+
reasoning=f"Keyword match (score: {best_score})"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _get_model_recommendation(
|
|
349
|
+
self,
|
|
350
|
+
category: TaskCategory,
|
|
351
|
+
complexity: TaskComplexity
|
|
352
|
+
) -> str:
|
|
353
|
+
"""Get recommended model for task
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
category: Task category
|
|
357
|
+
complexity: Task complexity
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Model name (fast, coding, reasoning, general)
|
|
361
|
+
"""
|
|
362
|
+
# Check specific recommendations
|
|
363
|
+
key = (category, complexity)
|
|
364
|
+
if key in self.MODEL_RECOMMENDATIONS:
|
|
365
|
+
return self.MODEL_RECOMMENDATIONS[key]
|
|
366
|
+
|
|
367
|
+
# Default recommendations by category
|
|
368
|
+
category_defaults = {
|
|
369
|
+
TaskCategory.CODE_GENERATION: "coding",
|
|
370
|
+
TaskCategory.CODE_EXPLANATION: "general",
|
|
371
|
+
TaskCategory.CODE_REFACTORING: "coding",
|
|
372
|
+
TaskCategory.CODE_DEBUGGING: "coding",
|
|
373
|
+
TaskCategory.FILE_OPERATIONS: "tools",
|
|
374
|
+
TaskCategory.SHELL_COMMANDS: "tools",
|
|
375
|
+
TaskCategory.GIT_OPERATIONS: "tools",
|
|
376
|
+
TaskCategory.DOCKER_OPERATIONS: "tools",
|
|
377
|
+
TaskCategory.PROJECT_SETUP: "coding",
|
|
378
|
+
TaskCategory.DOCUMENTATION: "general",
|
|
379
|
+
TaskCategory.TESTING: "coding",
|
|
380
|
+
TaskCategory.ARCHITECTURE: "reasoning",
|
|
381
|
+
TaskCategory.GENERAL_QA: "general",
|
|
382
|
+
TaskCategory.QUICK_ANSWER: "fast"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return category_defaults.get(category, "general")
|
|
386
|
+
|
|
387
|
+
def get_task_summary(self, task: str) -> str:
|
|
388
|
+
"""Get a brief summary of task classification
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
task: User's task
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Human-readable summary
|
|
395
|
+
"""
|
|
396
|
+
result = self.classify(task, use_llm=False) # Fast keyword classification
|
|
397
|
+
|
|
398
|
+
complexity_icons = {
|
|
399
|
+
TaskComplexity.TRIVIAL: "⚡",
|
|
400
|
+
TaskComplexity.SIMPLE: "🟢",
|
|
401
|
+
TaskComplexity.MODERATE: "🟡",
|
|
402
|
+
TaskComplexity.COMPLEX: "🟠",
|
|
403
|
+
TaskComplexity.EXPERT: "🔴"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
icon = complexity_icons.get(result.complexity, "⚪")
|
|
407
|
+
|
|
408
|
+
return f"{icon} {result.category.value.replace('_', ' ').title()} ({result.complexity.value})"
|
nc1709/version_check.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version Check - Automatic update notification for NC1709
|
|
3
|
+
|
|
4
|
+
Checks PyPI for newer versions and notifies users on startup.
|
|
5
|
+
Caches the check result to avoid repeated API calls.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Tuple
|
|
13
|
+
from packaging import version
|
|
14
|
+
|
|
15
|
+
# Cache settings
|
|
16
|
+
CACHE_DIR = Path.home() / ".nc1709"
|
|
17
|
+
CACHE_FILE = CACHE_DIR / "version_cache.json"
|
|
18
|
+
CACHE_TTL = 86400 # Check once per day (24 hours in seconds)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_current_version() -> str:
|
|
22
|
+
"""Get the currently installed version."""
|
|
23
|
+
from . import __version__
|
|
24
|
+
return __version__
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_latest_version_from_pypi() -> Optional[str]:
|
|
28
|
+
"""Fetch the latest version from PyPI.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Latest version string or None if fetch fails
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
import urllib.request
|
|
35
|
+
import urllib.error
|
|
36
|
+
|
|
37
|
+
url = "https://pypi.org/pypi/nc1709/json"
|
|
38
|
+
|
|
39
|
+
# Set a short timeout to not delay startup
|
|
40
|
+
request = urllib.request.Request(
|
|
41
|
+
url,
|
|
42
|
+
headers={"Accept": "application/json"}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
with urllib.request.urlopen(request, timeout=3) as response:
|
|
46
|
+
data = json.loads(response.read().decode())
|
|
47
|
+
return data.get("info", {}).get("version")
|
|
48
|
+
except Exception:
|
|
49
|
+
# Silently fail - don't interrupt the user experience
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_cache() -> dict:
|
|
54
|
+
"""Load the version check cache."""
|
|
55
|
+
try:
|
|
56
|
+
if CACHE_FILE.exists():
|
|
57
|
+
with open(CACHE_FILE, "r") as f:
|
|
58
|
+
return json.load(f)
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def save_cache(cache: dict) -> None:
|
|
65
|
+
"""Save the version check cache."""
|
|
66
|
+
try:
|
|
67
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
with open(CACHE_FILE, "w") as f:
|
|
69
|
+
json.dump(cache, f)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_for_update(force: bool = False) -> Tuple[bool, Optional[str], Optional[str]]:
|
|
75
|
+
"""Check if a newer version is available.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
force: If True, bypass cache and check PyPI directly
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Tuple of (update_available, current_version, latest_version)
|
|
82
|
+
"""
|
|
83
|
+
current = get_current_version()
|
|
84
|
+
|
|
85
|
+
# Check cache first (unless forced)
|
|
86
|
+
if not force:
|
|
87
|
+
cache = load_cache()
|
|
88
|
+
cache_time = cache.get("timestamp", 0)
|
|
89
|
+
|
|
90
|
+
if time.time() - cache_time < CACHE_TTL:
|
|
91
|
+
# Use cached result
|
|
92
|
+
latest = cache.get("latest_version")
|
|
93
|
+
if latest:
|
|
94
|
+
try:
|
|
95
|
+
update_available = version.parse(latest) > version.parse(current)
|
|
96
|
+
return (update_available, current, latest)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# Fetch from PyPI
|
|
101
|
+
latest = get_latest_version_from_pypi()
|
|
102
|
+
|
|
103
|
+
if latest:
|
|
104
|
+
# Update cache
|
|
105
|
+
save_cache({
|
|
106
|
+
"timestamp": time.time(),
|
|
107
|
+
"latest_version": latest,
|
|
108
|
+
"current_version": current
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
update_available = version.parse(latest) > version.parse(current)
|
|
113
|
+
return (update_available, current, latest)
|
|
114
|
+
except Exception:
|
|
115
|
+
return (False, current, latest)
|
|
116
|
+
|
|
117
|
+
return (False, current, None)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_update_message(current: str, latest: str) -> str:
|
|
121
|
+
"""Generate a user-friendly update message.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
current: Current installed version
|
|
125
|
+
latest: Latest available version
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Formatted update message
|
|
129
|
+
"""
|
|
130
|
+
return f"""
|
|
131
|
+
╭─────────────────────────────────────────────────────────────╮
|
|
132
|
+
│ 🆕 NC1709 Update Available! │
|
|
133
|
+
│ │
|
|
134
|
+
│ Current version: {current:<10} │
|
|
135
|
+
│ Latest version: {latest:<10} │
|
|
136
|
+
│ │
|
|
137
|
+
│ Run to update: pip install --upgrade nc1709 │
|
|
138
|
+
╰─────────────────────────────────────────────────────────────╯
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_and_notify(quiet: bool = False) -> Optional[str]:
|
|
143
|
+
"""Check for updates and return notification message if available.
|
|
144
|
+
|
|
145
|
+
This is the main function to call on CLI startup.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
quiet: If True, return None even if update available
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Update message string if update available, None otherwise
|
|
152
|
+
"""
|
|
153
|
+
if quiet:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
update_available, current, latest = check_for_update()
|
|
158
|
+
|
|
159
|
+
if update_available and current and latest:
|
|
160
|
+
return get_update_message(current, latest)
|
|
161
|
+
except Exception:
|
|
162
|
+
# Never crash the CLI due to version check
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def print_update_notification() -> None:
|
|
169
|
+
"""Print update notification if available.
|
|
170
|
+
|
|
171
|
+
Call this at CLI startup for automatic notifications.
|
|
172
|
+
"""
|
|
173
|
+
from .cli_ui import Color
|
|
174
|
+
|
|
175
|
+
message = check_and_notify()
|
|
176
|
+
if message:
|
|
177
|
+
print(f"{Color.YELLOW}{message}{Color.RESET}")
|