cognautic-cli 1.1.1__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.
- cognautic/__init__.py +7 -0
- cognautic/ai_engine.py +2213 -0
- cognautic/auto_continuation.py +196 -0
- cognautic/cli.py +1064 -0
- cognautic/config.py +245 -0
- cognautic/file_tagger.py +194 -0
- cognautic/memory.py +419 -0
- cognautic/provider_endpoints.py +424 -0
- cognautic/rules.py +246 -0
- cognautic/tools/__init__.py +19 -0
- cognautic/tools/base.py +59 -0
- cognautic/tools/code_analysis.py +391 -0
- cognautic/tools/command_runner.py +292 -0
- cognautic/tools/file_operations.py +394 -0
- cognautic/tools/registry.py +115 -0
- cognautic/tools/response_control.py +48 -0
- cognautic/tools/web_search.py +336 -0
- cognautic/utils.py +297 -0
- cognautic/websocket_server.py +485 -0
- cognautic_cli-1.1.1.dist-info/METADATA +604 -0
- cognautic_cli-1.1.1.dist-info/RECORD +25 -0
- cognautic_cli-1.1.1.dist-info/WHEEL +5 -0
- cognautic_cli-1.1.1.dist-info/entry_points.txt +2 -0
- cognautic_cli-1.1.1.dist-info/licenses/LICENSE +21 -0
- cognautic_cli-1.1.1.dist-info/top_level.txt +1 -0
cognautic/ai_engine.py
ADDED
|
@@ -0,0 +1,2213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Engine for multi-provider AI interactions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Dict, Any, Optional, List
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .config import ConfigManager
|
|
12
|
+
from .tools import ToolRegistry
|
|
13
|
+
from .provider_endpoints import GenericAPIClient, get_all_providers, get_provider_config
|
|
14
|
+
from .auto_continuation import AutoContinuationManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AIProvider(ABC):
|
|
18
|
+
"""Abstract base class for AI providers"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, api_key: str):
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def generate_response(
|
|
25
|
+
self, messages: List[Dict], model: str = None, **kwargs
|
|
26
|
+
) -> str:
|
|
27
|
+
"""Generate response from the AI provider"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def get_available_models(self) -> List[str]:
|
|
32
|
+
"""Get list of available models"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GenericProvider(AIProvider):
|
|
37
|
+
"""Generic provider that can work with any API endpoint"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, api_key: str, provider_name: str):
|
|
40
|
+
super().__init__(api_key)
|
|
41
|
+
self.provider_name = provider_name
|
|
42
|
+
self.client = GenericAPIClient(provider_name, api_key)
|
|
43
|
+
|
|
44
|
+
async def generate_response(
|
|
45
|
+
self, messages: List[Dict], model: str = None, **kwargs
|
|
46
|
+
) -> str:
|
|
47
|
+
try:
|
|
48
|
+
response = await self.client.chat_completion(messages, model, **kwargs)
|
|
49
|
+
|
|
50
|
+
# Extract text from different response formats
|
|
51
|
+
if self.provider_name == "google":
|
|
52
|
+
if "candidates" in response and response["candidates"]:
|
|
53
|
+
candidate = response["candidates"][0]
|
|
54
|
+
if "content" in candidate and "parts" in candidate["content"]:
|
|
55
|
+
return candidate["content"]["parts"][0]["text"]
|
|
56
|
+
return "No response generated"
|
|
57
|
+
|
|
58
|
+
elif self.provider_name == "anthropic":
|
|
59
|
+
if "content" in response and response["content"]:
|
|
60
|
+
return response["content"][0]["text"]
|
|
61
|
+
return "No response generated"
|
|
62
|
+
|
|
63
|
+
else:
|
|
64
|
+
# OpenAI-compatible format
|
|
65
|
+
if "choices" in response and response["choices"]:
|
|
66
|
+
return response["choices"][0]["message"]["content"]
|
|
67
|
+
return "No response generated"
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise Exception(f"{self.provider_name.title()} API error: {str(e)}")
|
|
71
|
+
|
|
72
|
+
def get_available_models(self) -> List[str]:
|
|
73
|
+
"""Get list of available models - returns generic list since we support any model"""
|
|
74
|
+
return [f"{self.provider_name}-model-1", f"{self.provider_name}-model-2"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class OpenAIProvider(AIProvider):
|
|
78
|
+
"""OpenAI provider implementation"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, api_key: str):
|
|
81
|
+
super().__init__(api_key)
|
|
82
|
+
try:
|
|
83
|
+
import openai
|
|
84
|
+
|
|
85
|
+
self.client = openai.AsyncOpenAI(api_key=api_key)
|
|
86
|
+
except ImportError:
|
|
87
|
+
raise ImportError("OpenAI library not installed. Run: pip install openai")
|
|
88
|
+
|
|
89
|
+
async def generate_response(
|
|
90
|
+
self, messages: List[Dict], model: str = "gpt-4", **kwargs
|
|
91
|
+
) -> str:
|
|
92
|
+
try:
|
|
93
|
+
response = await self.client.chat.completions.create(
|
|
94
|
+
model=model,
|
|
95
|
+
messages=messages,
|
|
96
|
+
max_tokens=kwargs.get("max_tokens", 4096),
|
|
97
|
+
temperature=kwargs.get("temperature", 0.7),
|
|
98
|
+
)
|
|
99
|
+
return response.choices[0].message.content
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise Exception(f"OpenAI API error: {str(e)}")
|
|
102
|
+
|
|
103
|
+
def get_available_models(self) -> List[str]:
|
|
104
|
+
return ["gpt-4", "gpt-4-turbo", "gpt-3.5-turbo", "gpt-4o", "gpt-4o-mini"]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AnthropicProvider(AIProvider):
|
|
108
|
+
"""Anthropic provider implementation"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, api_key: str):
|
|
111
|
+
super().__init__(api_key)
|
|
112
|
+
try:
|
|
113
|
+
import anthropic
|
|
114
|
+
|
|
115
|
+
self.client = anthropic.AsyncAnthropic(api_key=api_key)
|
|
116
|
+
except ImportError:
|
|
117
|
+
raise ImportError(
|
|
118
|
+
"Anthropic library not installed. Run: pip install anthropic"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def generate_response(
|
|
122
|
+
self, messages: List[Dict], model: str = "claude-3-sonnet-20240229", **kwargs
|
|
123
|
+
) -> str:
|
|
124
|
+
try:
|
|
125
|
+
# Convert messages format for Anthropic
|
|
126
|
+
system_message = ""
|
|
127
|
+
user_messages = []
|
|
128
|
+
|
|
129
|
+
for msg in messages:
|
|
130
|
+
if msg["role"] == "system":
|
|
131
|
+
system_message = msg["content"]
|
|
132
|
+
else:
|
|
133
|
+
user_messages.append(msg)
|
|
134
|
+
|
|
135
|
+
response = await self.client.messages.create(
|
|
136
|
+
model=model,
|
|
137
|
+
max_tokens=kwargs.get("max_tokens", 4096),
|
|
138
|
+
temperature=kwargs.get("temperature", 0.7),
|
|
139
|
+
system=system_message,
|
|
140
|
+
messages=user_messages,
|
|
141
|
+
)
|
|
142
|
+
return response.content[0].text
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise Exception(f"Anthropic API error: {str(e)}")
|
|
145
|
+
|
|
146
|
+
def get_available_models(self) -> List[str]:
|
|
147
|
+
return [
|
|
148
|
+
"claude-3-opus-20240229",
|
|
149
|
+
"claude-3-sonnet-20240229",
|
|
150
|
+
"claude-3-haiku-20240307",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class GoogleProvider(AIProvider):
|
|
155
|
+
"""Google provider implementation"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, api_key: str):
|
|
158
|
+
super().__init__(api_key)
|
|
159
|
+
try:
|
|
160
|
+
import google.generativeai as genai
|
|
161
|
+
|
|
162
|
+
genai.configure(api_key=api_key)
|
|
163
|
+
self.genai = genai
|
|
164
|
+
except ImportError:
|
|
165
|
+
raise ImportError(
|
|
166
|
+
"Google Generative AI library not installed. Run: pip install google-generativeai"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async def generate_response(
|
|
170
|
+
self, messages: List[Dict], model: str = "gemini-pro", **kwargs
|
|
171
|
+
) -> str:
|
|
172
|
+
try:
|
|
173
|
+
model_instance = self.genai.GenerativeModel(model)
|
|
174
|
+
|
|
175
|
+
# Convert messages to Google format
|
|
176
|
+
prompt = ""
|
|
177
|
+
for msg in messages:
|
|
178
|
+
if msg["role"] == "system":
|
|
179
|
+
prompt += f"System: {msg['content']}\n"
|
|
180
|
+
elif msg["role"] == "user":
|
|
181
|
+
prompt += f"User: {msg['content']}\n"
|
|
182
|
+
elif msg["role"] == "assistant":
|
|
183
|
+
prompt += f"Assistant: {msg['content']}\n"
|
|
184
|
+
|
|
185
|
+
response = await model_instance.generate_content_async(
|
|
186
|
+
prompt,
|
|
187
|
+
generation_config=self.genai.types.GenerationConfig(
|
|
188
|
+
max_output_tokens=kwargs.get("max_tokens", 4096),
|
|
189
|
+
temperature=kwargs.get("temperature", 0.7),
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
return response.text
|
|
193
|
+
except Exception as e:
|
|
194
|
+
raise Exception(f"Google API error: {str(e)}")
|
|
195
|
+
|
|
196
|
+
async def generate_response_stream(
|
|
197
|
+
self, messages: List[Dict], model: str = "gemini-pro", **kwargs
|
|
198
|
+
):
|
|
199
|
+
"""Generate streaming response"""
|
|
200
|
+
try:
|
|
201
|
+
model_instance = self.genai.GenerativeModel(model)
|
|
202
|
+
|
|
203
|
+
# Convert messages to Google format
|
|
204
|
+
prompt = ""
|
|
205
|
+
for msg in messages:
|
|
206
|
+
if msg["role"] == "system":
|
|
207
|
+
prompt += f"System: {msg['content']}\n"
|
|
208
|
+
elif msg["role"] == "user":
|
|
209
|
+
prompt += f"User: {msg['content']}\n"
|
|
210
|
+
elif msg["role"] == "assistant":
|
|
211
|
+
prompt += f"Assistant: {msg['content']}\n"
|
|
212
|
+
|
|
213
|
+
response = model_instance.generate_content(
|
|
214
|
+
prompt,
|
|
215
|
+
generation_config=self.genai.types.GenerationConfig(
|
|
216
|
+
max_output_tokens=kwargs.get("max_tokens", 4096),
|
|
217
|
+
temperature=kwargs.get("temperature", 0.7),
|
|
218
|
+
),
|
|
219
|
+
stream=True,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
for chunk in response:
|
|
223
|
+
if chunk.text:
|
|
224
|
+
yield chunk.text
|
|
225
|
+
except Exception as e:
|
|
226
|
+
raise Exception(f"Google API error: {str(e)}")
|
|
227
|
+
|
|
228
|
+
def get_available_models(self) -> List[str]:
|
|
229
|
+
return ["gemini-1.5-flash", "gemini-1.5-pro", "gemini-pro", "gemini-pro-vision"]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TogetherProvider(AIProvider):
|
|
233
|
+
"""Together AI provider implementation"""
|
|
234
|
+
|
|
235
|
+
def __init__(self, api_key: str):
|
|
236
|
+
super().__init__(api_key)
|
|
237
|
+
try:
|
|
238
|
+
import together
|
|
239
|
+
|
|
240
|
+
self.client = together.AsyncTogether(api_key=api_key)
|
|
241
|
+
except ImportError:
|
|
242
|
+
raise ImportError(
|
|
243
|
+
"Together library not installed. Run: pip install together"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
async def generate_response(
|
|
247
|
+
self,
|
|
248
|
+
messages: List[Dict],
|
|
249
|
+
model: str = "meta-llama/Llama-2-70b-chat-hf",
|
|
250
|
+
**kwargs,
|
|
251
|
+
) -> str:
|
|
252
|
+
try:
|
|
253
|
+
response = await self.client.chat.completions.create(
|
|
254
|
+
model=model,
|
|
255
|
+
messages=messages,
|
|
256
|
+
max_tokens=kwargs.get("max_tokens", 4096),
|
|
257
|
+
temperature=kwargs.get("temperature", 0.7),
|
|
258
|
+
)
|
|
259
|
+
return response.choices[0].message.content
|
|
260
|
+
except Exception as e:
|
|
261
|
+
raise Exception(f"Together API error: {str(e)}")
|
|
262
|
+
|
|
263
|
+
def get_available_models(self) -> List[str]:
|
|
264
|
+
return [
|
|
265
|
+
"meta-llama/Llama-2-70b-chat-hf",
|
|
266
|
+
"meta-llama/Llama-2-13b-chat-hf",
|
|
267
|
+
"mistralai/Mixtral-8x7B-Instruct-v0.1",
|
|
268
|
+
"NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class OpenRouterProvider(AIProvider):
|
|
273
|
+
"""OpenRouter provider implementation"""
|
|
274
|
+
|
|
275
|
+
def __init__(self, api_key: str):
|
|
276
|
+
super().__init__(api_key)
|
|
277
|
+
try:
|
|
278
|
+
import openai
|
|
279
|
+
|
|
280
|
+
self.client = openai.AsyncOpenAI(
|
|
281
|
+
api_key=api_key, base_url="https://openrouter.ai/api/v1"
|
|
282
|
+
)
|
|
283
|
+
except ImportError:
|
|
284
|
+
raise ImportError("OpenAI library not installed. Run: pip install openai")
|
|
285
|
+
|
|
286
|
+
async def generate_response(
|
|
287
|
+
self, messages: List[Dict], model: str = "anthropic/claude-3-sonnet", **kwargs
|
|
288
|
+
) -> str:
|
|
289
|
+
try:
|
|
290
|
+
response = await self.client.chat.completions.create(
|
|
291
|
+
model=model,
|
|
292
|
+
messages=messages,
|
|
293
|
+
max_tokens=kwargs.get("max_tokens", 4096),
|
|
294
|
+
temperature=kwargs.get("temperature", 0.7),
|
|
295
|
+
)
|
|
296
|
+
return response.choices[0].message.content
|
|
297
|
+
except Exception as e:
|
|
298
|
+
raise Exception(f"OpenRouter API error: {str(e)}")
|
|
299
|
+
|
|
300
|
+
def get_available_models(self) -> List[str]:
|
|
301
|
+
return [
|
|
302
|
+
"anthropic/claude-3-opus",
|
|
303
|
+
"anthropic/claude-3-sonnet",
|
|
304
|
+
"openai/gpt-4-turbo",
|
|
305
|
+
"meta-llama/llama-3-70b-instruct",
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class LocalModelProvider(AIProvider):
|
|
310
|
+
"""Local model provider for both Hugging Face and GGUF models"""
|
|
311
|
+
|
|
312
|
+
def __init__(self, model_path: str):
|
|
313
|
+
super().__init__(api_key="local") # No API key needed for local models
|
|
314
|
+
self.model_path = model_path
|
|
315
|
+
self.model = None
|
|
316
|
+
self.tokenizer = None
|
|
317
|
+
self.device = None
|
|
318
|
+
self.is_gguf = model_path.endswith(".gguf")
|
|
319
|
+
self.n_ctx = 2048 # Will be set during model loading
|
|
320
|
+
self._load_model()
|
|
321
|
+
|
|
322
|
+
def _load_model(self):
|
|
323
|
+
"""Load the local model (Hugging Face or GGUF)"""
|
|
324
|
+
if self.is_gguf:
|
|
325
|
+
self._load_gguf_model()
|
|
326
|
+
else:
|
|
327
|
+
self._load_hf_model()
|
|
328
|
+
|
|
329
|
+
def _load_gguf_model(self):
|
|
330
|
+
"""Load a GGUF quantized model using llama-cpp-python"""
|
|
331
|
+
try:
|
|
332
|
+
from llama_cpp import Llama
|
|
333
|
+
import os
|
|
334
|
+
|
|
335
|
+
print(f"Loading GGUF model from {self.model_path}...")
|
|
336
|
+
|
|
337
|
+
# Get optimal thread count (use all available cores)
|
|
338
|
+
n_threads = os.cpu_count() or 4
|
|
339
|
+
|
|
340
|
+
# Try to load model metadata to get optimal context size
|
|
341
|
+
try:
|
|
342
|
+
# Load with minimal context first to read metadata
|
|
343
|
+
temp_model = Llama(model_path=self.model_path, n_ctx=512, verbose=False)
|
|
344
|
+
# Get model's training context (if available)
|
|
345
|
+
model_metadata = temp_model.metadata
|
|
346
|
+
n_ctx_train = (
|
|
347
|
+
model_metadata.get("n_ctx_train", 2048)
|
|
348
|
+
if hasattr(temp_model, "metadata")
|
|
349
|
+
else 2048
|
|
350
|
+
)
|
|
351
|
+
del temp_model
|
|
352
|
+
|
|
353
|
+
# Use smaller of: training context or 4096 (for performance)
|
|
354
|
+
self.n_ctx = min(n_ctx_train, 4096) if n_ctx_train > 0 else 2048
|
|
355
|
+
except:
|
|
356
|
+
# Fallback to 2048 if metadata reading fails
|
|
357
|
+
self.n_ctx = 2048
|
|
358
|
+
|
|
359
|
+
print(f"Using context window: {self.n_ctx} tokens")
|
|
360
|
+
|
|
361
|
+
# Load model with optimal context
|
|
362
|
+
self.model = Llama(
|
|
363
|
+
model_path=self.model_path,
|
|
364
|
+
n_ctx=self.n_ctx, # Auto-detected context window
|
|
365
|
+
n_threads=n_threads, # Use all CPU threads
|
|
366
|
+
n_gpu_layers=0, # 0 for CPU, increase for GPU
|
|
367
|
+
n_batch=512, # Batch size for prompt processing
|
|
368
|
+
verbose=False,
|
|
369
|
+
)
|
|
370
|
+
self.device = "cpu"
|
|
371
|
+
print(
|
|
372
|
+
f"✅ GGUF model loaded successfully on {self.device} ({n_threads} threads)"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
except ImportError:
|
|
376
|
+
raise ImportError(
|
|
377
|
+
"llama-cpp-python not installed. For GGUF models, run:\n"
|
|
378
|
+
" pip install llama-cpp-python\n"
|
|
379
|
+
"Or for GPU support:\n"
|
|
380
|
+
' CMAKE_ARGS="-DLLAMA_CUBLAS=on" pip install llama-cpp-python'
|
|
381
|
+
)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
raise Exception(f"Failed to load GGUF model: {str(e)}")
|
|
384
|
+
|
|
385
|
+
def _load_hf_model(self):
|
|
386
|
+
"""Load a Hugging Face transformers model"""
|
|
387
|
+
try:
|
|
388
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
389
|
+
import torch
|
|
390
|
+
|
|
391
|
+
# Determine device
|
|
392
|
+
if torch.cuda.is_available():
|
|
393
|
+
self.device = "cuda"
|
|
394
|
+
elif torch.backends.mps.is_available():
|
|
395
|
+
self.device = "mps"
|
|
396
|
+
else:
|
|
397
|
+
self.device = "cpu"
|
|
398
|
+
|
|
399
|
+
print(f"Loading local model from {self.model_path} on {self.device}...")
|
|
400
|
+
|
|
401
|
+
# Load tokenizer
|
|
402
|
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_path)
|
|
403
|
+
|
|
404
|
+
# Load model with appropriate settings
|
|
405
|
+
if self.device == "cuda":
|
|
406
|
+
self.model = AutoModelForCausalLM.from_pretrained(
|
|
407
|
+
self.model_path,
|
|
408
|
+
device_map="auto",
|
|
409
|
+
torch_dtype=torch.float16,
|
|
410
|
+
low_cpu_mem_usage=True,
|
|
411
|
+
)
|
|
412
|
+
else:
|
|
413
|
+
self.model = AutoModelForCausalLM.from_pretrained(
|
|
414
|
+
self.model_path, torch_dtype=torch.float32
|
|
415
|
+
)
|
|
416
|
+
self.model.to(self.device)
|
|
417
|
+
|
|
418
|
+
print(f"✅ Model loaded successfully on {self.device}")
|
|
419
|
+
|
|
420
|
+
except ImportError:
|
|
421
|
+
raise ImportError(
|
|
422
|
+
"Transformers and PyTorch not installed. Run: pip install transformers torch accelerate"
|
|
423
|
+
)
|
|
424
|
+
except Exception as e:
|
|
425
|
+
raise Exception(f"Failed to load local model: {str(e)}")
|
|
426
|
+
|
|
427
|
+
async def generate_response(
|
|
428
|
+
self, messages: List[Dict], model: str = None, **kwargs
|
|
429
|
+
) -> str:
|
|
430
|
+
"""Generate response from the local model"""
|
|
431
|
+
if self.is_gguf:
|
|
432
|
+
return await self._generate_gguf_response(messages, **kwargs)
|
|
433
|
+
else:
|
|
434
|
+
return await self._generate_hf_response(messages, **kwargs)
|
|
435
|
+
|
|
436
|
+
async def _generate_gguf_response(self, messages: List[Dict], **kwargs) -> str:
|
|
437
|
+
"""Generate response using GGUF model"""
|
|
438
|
+
try:
|
|
439
|
+
# Truncate messages if needed to fit context window
|
|
440
|
+
max_tokens = kwargs.get("max_tokens", 256) # Reduced for faster response
|
|
441
|
+
temperature = kwargs.get("temperature", 0.7)
|
|
442
|
+
|
|
443
|
+
# Use actual context window size with safety margin
|
|
444
|
+
# Reserve space for: response (max_tokens) + safety buffer (200)
|
|
445
|
+
available_context = self.n_ctx - max_tokens - 200
|
|
446
|
+
|
|
447
|
+
# Estimate tokens more conservatively (1 token ≈ 3 characters for safety)
|
|
448
|
+
max_prompt_chars = available_context * 3
|
|
449
|
+
|
|
450
|
+
# Truncate messages to fit
|
|
451
|
+
truncated_messages = self._truncate_messages(messages, max_prompt_chars)
|
|
452
|
+
|
|
453
|
+
# Convert messages to prompt
|
|
454
|
+
prompt = self._messages_to_prompt(truncated_messages)
|
|
455
|
+
|
|
456
|
+
# Double-check prompt length (rough token estimate)
|
|
457
|
+
estimated_prompt_tokens = len(prompt) // 3
|
|
458
|
+
if estimated_prompt_tokens + max_tokens > self.n_ctx:
|
|
459
|
+
# Emergency truncation - keep only last message
|
|
460
|
+
truncated_messages = messages[-1:] if messages else []
|
|
461
|
+
prompt = self._messages_to_prompt(truncated_messages)
|
|
462
|
+
|
|
463
|
+
# Generate response with optimized settings
|
|
464
|
+
response = self.model(
|
|
465
|
+
prompt,
|
|
466
|
+
max_tokens=max_tokens,
|
|
467
|
+
temperature=temperature,
|
|
468
|
+
top_p=0.95,
|
|
469
|
+
top_k=40,
|
|
470
|
+
repeat_penalty=1.1,
|
|
471
|
+
echo=False, # Don't echo the prompt
|
|
472
|
+
stop=["User:", "System:", "\n\n"], # Stop tokens
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Extract text from response
|
|
476
|
+
if isinstance(response, dict) and "choices" in response:
|
|
477
|
+
return response["choices"][0]["text"].strip()
|
|
478
|
+
return str(response).strip()
|
|
479
|
+
|
|
480
|
+
except Exception as e:
|
|
481
|
+
raise Exception(f"GGUF model generation error: {str(e)}")
|
|
482
|
+
|
|
483
|
+
async def _generate_hf_response(self, messages: List[Dict], **kwargs) -> str:
|
|
484
|
+
"""Generate response using Hugging Face model"""
|
|
485
|
+
try:
|
|
486
|
+
import torch
|
|
487
|
+
|
|
488
|
+
# Convert messages to a single prompt
|
|
489
|
+
prompt = self._messages_to_prompt(messages)
|
|
490
|
+
|
|
491
|
+
# Tokenize input
|
|
492
|
+
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
|
|
493
|
+
|
|
494
|
+
# Generate response
|
|
495
|
+
max_tokens = kwargs.get("max_tokens", 2048)
|
|
496
|
+
temperature = kwargs.get("temperature", 0.7)
|
|
497
|
+
|
|
498
|
+
with torch.no_grad():
|
|
499
|
+
outputs = self.model.generate(
|
|
500
|
+
**inputs,
|
|
501
|
+
max_new_tokens=max_tokens,
|
|
502
|
+
temperature=temperature,
|
|
503
|
+
do_sample=True,
|
|
504
|
+
top_p=0.9,
|
|
505
|
+
pad_token_id=self.tokenizer.eos_token_id,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Decode response
|
|
509
|
+
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
|
|
510
|
+
|
|
511
|
+
# Remove the prompt from the response
|
|
512
|
+
if response.startswith(prompt):
|
|
513
|
+
response = response[len(prompt) :].strip()
|
|
514
|
+
|
|
515
|
+
return response
|
|
516
|
+
|
|
517
|
+
except Exception as e:
|
|
518
|
+
raise Exception(f"Local model generation error: {str(e)}")
|
|
519
|
+
|
|
520
|
+
def _truncate_messages(self, messages: List[Dict], max_chars: int) -> List[Dict]:
|
|
521
|
+
"""Truncate messages to fit within character limit"""
|
|
522
|
+
# Always keep system message if present
|
|
523
|
+
system_msgs = [m for m in messages if m["role"] == "system"]
|
|
524
|
+
other_msgs = [m for m in messages if m["role"] != "system"]
|
|
525
|
+
|
|
526
|
+
# Estimate current size
|
|
527
|
+
total_chars = sum(len(m["content"]) for m in messages)
|
|
528
|
+
|
|
529
|
+
if total_chars <= max_chars:
|
|
530
|
+
return messages
|
|
531
|
+
|
|
532
|
+
# Keep system message and recent messages
|
|
533
|
+
result = system_msgs.copy()
|
|
534
|
+
current_chars = sum(len(m["content"]) for m in system_msgs)
|
|
535
|
+
|
|
536
|
+
# Add messages from most recent backwards
|
|
537
|
+
for msg in reversed(other_msgs):
|
|
538
|
+
msg_chars = len(msg["content"])
|
|
539
|
+
if current_chars + msg_chars <= max_chars:
|
|
540
|
+
result.insert(len(system_msgs), msg)
|
|
541
|
+
current_chars += msg_chars
|
|
542
|
+
else:
|
|
543
|
+
break
|
|
544
|
+
|
|
545
|
+
return result
|
|
546
|
+
|
|
547
|
+
def _messages_to_prompt(self, messages: List[Dict]) -> str:
|
|
548
|
+
"""Convert message format to a prompt string"""
|
|
549
|
+
prompt = ""
|
|
550
|
+
for msg in messages:
|
|
551
|
+
role = msg["role"]
|
|
552
|
+
content = msg["content"]
|
|
553
|
+
|
|
554
|
+
if role == "system":
|
|
555
|
+
prompt += f"System: {content}\n\n"
|
|
556
|
+
elif role == "user":
|
|
557
|
+
prompt += f"User: {content}\n\n"
|
|
558
|
+
elif role == "assistant":
|
|
559
|
+
prompt += f"Assistant: {content}\n\n"
|
|
560
|
+
|
|
561
|
+
# Add final assistant prompt
|
|
562
|
+
prompt += "Assistant: "
|
|
563
|
+
return prompt
|
|
564
|
+
|
|
565
|
+
def get_available_models(self) -> List[str]:
|
|
566
|
+
"""Get list of available models"""
|
|
567
|
+
return [self.model_path]
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class AIEngine:
|
|
571
|
+
"""Main AI engine that manages providers and handles requests"""
|
|
572
|
+
|
|
573
|
+
def __init__(self, config_manager: ConfigManager):
|
|
574
|
+
self.config_manager = config_manager
|
|
575
|
+
self.tool_registry = ToolRegistry()
|
|
576
|
+
self.providers = {}
|
|
577
|
+
self.local_model_provider = None # Track local model separately
|
|
578
|
+
self.auto_continuation = AutoContinuationManager(max_iterations=50)
|
|
579
|
+
# Give AI engine unrestricted permissions
|
|
580
|
+
from .tools.base import PermissionLevel
|
|
581
|
+
|
|
582
|
+
self.tool_registry.set_permission_level(
|
|
583
|
+
"ai_engine", PermissionLevel.UNRESTRICTED
|
|
584
|
+
)
|
|
585
|
+
self._initialize_providers()
|
|
586
|
+
|
|
587
|
+
def _initialize_providers(self):
|
|
588
|
+
"""Initialize available AI providers"""
|
|
589
|
+
# Get all supported providers from endpoints
|
|
590
|
+
all_providers = get_all_providers()
|
|
591
|
+
|
|
592
|
+
# Legacy provider classes for backward compatibility
|
|
593
|
+
legacy_provider_classes = {
|
|
594
|
+
"openai": OpenAIProvider,
|
|
595
|
+
"anthropic": AnthropicProvider,
|
|
596
|
+
"google": GoogleProvider,
|
|
597
|
+
"together": TogetherProvider,
|
|
598
|
+
"openrouter": OpenRouterProvider,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
for provider_name in all_providers:
|
|
602
|
+
api_key = self.config_manager.get_api_key(provider_name)
|
|
603
|
+
if api_key:
|
|
604
|
+
try:
|
|
605
|
+
# Use legacy provider if available, otherwise use generic provider
|
|
606
|
+
if provider_name in legacy_provider_classes:
|
|
607
|
+
self.providers[provider_name] = legacy_provider_classes[
|
|
608
|
+
provider_name
|
|
609
|
+
](api_key)
|
|
610
|
+
else:
|
|
611
|
+
self.providers[provider_name] = GenericProvider(
|
|
612
|
+
api_key, provider_name
|
|
613
|
+
)
|
|
614
|
+
except Exception as e:
|
|
615
|
+
print(f"Warning: Could not initialize {provider_name}: {e}")
|
|
616
|
+
|
|
617
|
+
# Don't auto-load local model at startup - only load when explicitly requested
|
|
618
|
+
# This prevents unnecessary loading and error messages when not using local provider
|
|
619
|
+
|
|
620
|
+
def load_local_model(self, model_path: str):
|
|
621
|
+
"""Load a local Hugging Face model"""
|
|
622
|
+
try:
|
|
623
|
+
self.local_model_provider = LocalModelProvider(model_path)
|
|
624
|
+
self.providers["local"] = self.local_model_provider
|
|
625
|
+
self.config_manager.set_config("local_model_path", model_path)
|
|
626
|
+
return True
|
|
627
|
+
except Exception as e:
|
|
628
|
+
raise Exception(f"Failed to load local model: {str(e)}")
|
|
629
|
+
|
|
630
|
+
def unload_local_model(self):
|
|
631
|
+
"""Unload the local model and free memory"""
|
|
632
|
+
if "local" in self.providers:
|
|
633
|
+
del self.providers["local"]
|
|
634
|
+
self.local_model_provider = None
|
|
635
|
+
self.config_manager.delete_config("local_model_path")
|
|
636
|
+
|
|
637
|
+
def get_available_providers(self) -> List[str]:
|
|
638
|
+
"""Get list of available providers"""
|
|
639
|
+
return list(self.providers.keys())
|
|
640
|
+
|
|
641
|
+
def get_provider_models(self, provider: str) -> List[str]:
|
|
642
|
+
"""Get available models for a provider"""
|
|
643
|
+
if provider in self.providers:
|
|
644
|
+
return self.providers[provider].get_available_models()
|
|
645
|
+
return []
|
|
646
|
+
|
|
647
|
+
async def process_message(
|
|
648
|
+
self,
|
|
649
|
+
message: str,
|
|
650
|
+
provider: str = None,
|
|
651
|
+
model: str = None,
|
|
652
|
+
project_path: str = None,
|
|
653
|
+
context: List[Dict] = None,
|
|
654
|
+
conversation_history: List[Dict] = None,
|
|
655
|
+
) -> str:
|
|
656
|
+
"""Process a user message and generate AI response"""
|
|
657
|
+
|
|
658
|
+
# Use default provider if not specified
|
|
659
|
+
if not provider:
|
|
660
|
+
provider = self.config_manager.get_config_value("default_provider")
|
|
661
|
+
|
|
662
|
+
if provider not in self.providers:
|
|
663
|
+
raise Exception(
|
|
664
|
+
f"Provider '{provider}' not available. Available: {list(self.providers.keys())}"
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# Build message context
|
|
668
|
+
messages = []
|
|
669
|
+
|
|
670
|
+
# Add system message
|
|
671
|
+
system_prompt = self._build_system_prompt(project_path)
|
|
672
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
673
|
+
|
|
674
|
+
# Add conversation history if provided (for context)
|
|
675
|
+
if conversation_history:
|
|
676
|
+
messages.extend(conversation_history)
|
|
677
|
+
|
|
678
|
+
# Add context if provided (for additional context)
|
|
679
|
+
if context:
|
|
680
|
+
messages.extend(context)
|
|
681
|
+
|
|
682
|
+
# Add current user message
|
|
683
|
+
messages.append({"role": "user", "content": message})
|
|
684
|
+
|
|
685
|
+
# Get AI response
|
|
686
|
+
ai_provider = self.providers[provider]
|
|
687
|
+
|
|
688
|
+
# Use default model if not specified
|
|
689
|
+
if not model:
|
|
690
|
+
available_models = ai_provider.get_available_models()
|
|
691
|
+
if provider == "google":
|
|
692
|
+
model = (
|
|
693
|
+
available_models[0] if available_models else "gemini-1.5-flash"
|
|
694
|
+
) # Use newer model
|
|
695
|
+
else:
|
|
696
|
+
model = available_models[0] if available_models else None
|
|
697
|
+
|
|
698
|
+
config = self.config_manager.get_config()
|
|
699
|
+
# Use unlimited tokens (or max available for the model)
|
|
700
|
+
max_tokens = config.get("max_tokens", None) # None = unlimited
|
|
701
|
+
if max_tokens == 0 or max_tokens == -1:
|
|
702
|
+
max_tokens = None # Treat 0 or -1 as unlimited
|
|
703
|
+
|
|
704
|
+
response = await ai_provider.generate_response(
|
|
705
|
+
messages=messages,
|
|
706
|
+
model=model,
|
|
707
|
+
max_tokens=max_tokens or 16384, # Use large default if None
|
|
708
|
+
temperature=config.get("temperature", 0.7),
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# Check if response contains tool calls
|
|
712
|
+
has_tools = self._contains_tool_calls(response)
|
|
713
|
+
if has_tools:
|
|
714
|
+
response = await self._execute_tools(response, project_path)
|
|
715
|
+
|
|
716
|
+
return response
|
|
717
|
+
|
|
718
|
+
async def process_message_stream(
|
|
719
|
+
self,
|
|
720
|
+
message: str,
|
|
721
|
+
provider: str = None,
|
|
722
|
+
model: str = None,
|
|
723
|
+
project_path: str = None,
|
|
724
|
+
context: List[Dict] = None,
|
|
725
|
+
conversation_history: List[Dict] = None,
|
|
726
|
+
):
|
|
727
|
+
"""Process a user message and generate streaming AI response"""
|
|
728
|
+
|
|
729
|
+
# Reset auto-continuation counter for new message
|
|
730
|
+
self.auto_continuation.reset()
|
|
731
|
+
|
|
732
|
+
# Use default provider if not specified
|
|
733
|
+
if not provider:
|
|
734
|
+
provider = self.config_manager.get_config_value("default_provider")
|
|
735
|
+
|
|
736
|
+
if provider not in self.providers:
|
|
737
|
+
raise Exception(
|
|
738
|
+
f"Provider '{provider}' not available. Available: {list(self.providers.keys())}"
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# No file tagging - use message as-is
|
|
742
|
+
processed_message = message
|
|
743
|
+
|
|
744
|
+
# Build message context
|
|
745
|
+
messages = []
|
|
746
|
+
|
|
747
|
+
# Add system message
|
|
748
|
+
system_prompt = self._build_system_prompt(project_path)
|
|
749
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
750
|
+
|
|
751
|
+
# Add conversation history if provided (for context)
|
|
752
|
+
if conversation_history:
|
|
753
|
+
messages.extend(conversation_history)
|
|
754
|
+
|
|
755
|
+
# Add context if provided (for additional context)
|
|
756
|
+
if context:
|
|
757
|
+
messages.extend(context)
|
|
758
|
+
|
|
759
|
+
# Add current user message
|
|
760
|
+
messages.append({"role": "user", "content": processed_message})
|
|
761
|
+
|
|
762
|
+
# Get AI response
|
|
763
|
+
ai_provider = self.providers[provider]
|
|
764
|
+
|
|
765
|
+
# Use default model if not specified
|
|
766
|
+
if not model:
|
|
767
|
+
available_models = ai_provider.get_available_models()
|
|
768
|
+
if provider == "google":
|
|
769
|
+
model = (
|
|
770
|
+
available_models[0] if available_models else "gemini-1.5-flash"
|
|
771
|
+
) # Use newer model
|
|
772
|
+
else:
|
|
773
|
+
model = available_models[0] if available_models else None
|
|
774
|
+
|
|
775
|
+
config = self.config_manager.get_config()
|
|
776
|
+
|
|
777
|
+
# Use unlimited tokens (or max available for the model)
|
|
778
|
+
max_tokens = config.get("max_tokens", None) # None = unlimited
|
|
779
|
+
if max_tokens == 0 or max_tokens == -1:
|
|
780
|
+
max_tokens = None # Treat 0 or -1 as unlimited
|
|
781
|
+
|
|
782
|
+
# Generate response and process with tools
|
|
783
|
+
response = await ai_provider.generate_response(
|
|
784
|
+
messages=messages,
|
|
785
|
+
model=model,
|
|
786
|
+
max_tokens=max_tokens or 16384, # Use large default if None
|
|
787
|
+
temperature=config.get("temperature", 0.7),
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Process response with tools
|
|
791
|
+
async for chunk in self._process_response_with_tools(
|
|
792
|
+
response, project_path, messages, ai_provider, model, config
|
|
793
|
+
):
|
|
794
|
+
yield chunk
|
|
795
|
+
|
|
796
|
+
def _parse_alternative_tool_calls(self, response: str):
|
|
797
|
+
"""Parse tool calls from alternative formats (e.g., GPT-OSS with special tokens)"""
|
|
798
|
+
import re
|
|
799
|
+
import json
|
|
800
|
+
|
|
801
|
+
tool_calls = []
|
|
802
|
+
|
|
803
|
+
# Pattern 1: GPT-OSS format with <|message|> tags containing JSON
|
|
804
|
+
# Example: <|message|>{"command":"ls -la"}<|call|>
|
|
805
|
+
gpt_oss_pattern = r"<\|message\|>(.*?)<\|call\|>"
|
|
806
|
+
gpt_oss_matches = re.findall(gpt_oss_pattern, response, re.DOTALL)
|
|
807
|
+
|
|
808
|
+
for match in gpt_oss_matches:
|
|
809
|
+
try:
|
|
810
|
+
# Try to parse as JSON
|
|
811
|
+
data = json.loads(match.strip())
|
|
812
|
+
|
|
813
|
+
# Determine tool type based on keys
|
|
814
|
+
if "command" in data:
|
|
815
|
+
tool_calls.append(
|
|
816
|
+
{
|
|
817
|
+
"tool_code": "command_runner",
|
|
818
|
+
"args": {"command": data["command"]},
|
|
819
|
+
}
|
|
820
|
+
)
|
|
821
|
+
elif "operation" in data:
|
|
822
|
+
tool_calls.append({"tool_code": "file_operations", "args": data})
|
|
823
|
+
else:
|
|
824
|
+
# Generic tool call
|
|
825
|
+
tool_calls.append(
|
|
826
|
+
{"tool_code": data.get("tool_code", "unknown"), "args": data}
|
|
827
|
+
)
|
|
828
|
+
except json.JSONDecodeError:
|
|
829
|
+
continue
|
|
830
|
+
|
|
831
|
+
# Pattern 2: Look for channel indicators with tool names
|
|
832
|
+
# Example: <|channel|>commentary to=command_runner
|
|
833
|
+
channel_pattern = r"<\|channel\|>.*?to=(\w+)"
|
|
834
|
+
channels = re.findall(channel_pattern, response)
|
|
835
|
+
|
|
836
|
+
# Match channels with their corresponding messages
|
|
837
|
+
if channels and gpt_oss_matches:
|
|
838
|
+
for i, (channel, match) in enumerate(zip(channels, gpt_oss_matches)):
|
|
839
|
+
if i < len(tool_calls):
|
|
840
|
+
# Update tool_code based on channel
|
|
841
|
+
if "command_runner" in channel:
|
|
842
|
+
tool_calls[i]["tool_code"] = "command_runner"
|
|
843
|
+
elif (
|
|
844
|
+
"command_operations" in channel or "file_operations" in channel
|
|
845
|
+
):
|
|
846
|
+
tool_calls[i]["tool_code"] = "file_operations"
|
|
847
|
+
elif "response_control" in channel:
|
|
848
|
+
tool_calls[i]["tool_code"] = "response_control"
|
|
849
|
+
|
|
850
|
+
return tool_calls
|
|
851
|
+
|
|
852
|
+
def _clean_model_syntax(self, text: str) -> str:
|
|
853
|
+
"""Remove model-specific syntax tokens from response"""
|
|
854
|
+
import re
|
|
855
|
+
|
|
856
|
+
# Remove common model-specific tokens
|
|
857
|
+
patterns = [
|
|
858
|
+
r'<\|start\|>',
|
|
859
|
+
r'<\|end\|>',
|
|
860
|
+
r'<\|channel\|>',
|
|
861
|
+
r'<\|message\|>',
|
|
862
|
+
r'<\|call\|>',
|
|
863
|
+
r'<\|calls\|>',
|
|
864
|
+
r'assistant\s+to=\w+\s+code',
|
|
865
|
+
r'analysis\s+to=\w+\s+code',
|
|
866
|
+
r'<\|[^|]+\|>', # Any other pipe-delimited tokens
|
|
867
|
+
]
|
|
868
|
+
|
|
869
|
+
cleaned = text
|
|
870
|
+
for pattern in patterns:
|
|
871
|
+
cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE)
|
|
872
|
+
|
|
873
|
+
# Remove multiple consecutive newlines left by token removal
|
|
874
|
+
cleaned = re.sub(r'\n{3,}', '\n\n', cleaned)
|
|
875
|
+
|
|
876
|
+
return cleaned.strip()
|
|
877
|
+
|
|
878
|
+
async def _process_response_with_tools(
|
|
879
|
+
self,
|
|
880
|
+
response: str,
|
|
881
|
+
project_path: str,
|
|
882
|
+
messages: list,
|
|
883
|
+
ai_provider,
|
|
884
|
+
model: str,
|
|
885
|
+
config: dict,
|
|
886
|
+
recursion_depth: int = 0,
|
|
887
|
+
):
|
|
888
|
+
"""Process response and execute tools with AI continuation"""
|
|
889
|
+
import re
|
|
890
|
+
import json
|
|
891
|
+
|
|
892
|
+
# Clean model-specific syntax tokens from response
|
|
893
|
+
response = self._clean_model_syntax(response)
|
|
894
|
+
|
|
895
|
+
# Limit recursion to prevent infinite loops
|
|
896
|
+
MAX_RECURSION_DEPTH = 999999 # Effectively unlimited - AI will continue until task is complete
|
|
897
|
+
if recursion_depth >= MAX_RECURSION_DEPTH:
|
|
898
|
+
# Silently stop recursion without warning
|
|
899
|
+
yield response
|
|
900
|
+
yield "\n\n⚠️ **Maximum continuation depth reached. Please continue manually if needed.**\n"
|
|
901
|
+
return
|
|
902
|
+
|
|
903
|
+
# Find JSON tool calls in the response (standard format)
|
|
904
|
+
json_pattern = r"```json\s*\n(.*?)\n```"
|
|
905
|
+
matches = re.findall(json_pattern, response, re.DOTALL)
|
|
906
|
+
|
|
907
|
+
# Also check for alternative formats (e.g., GPT-OSS)
|
|
908
|
+
alt_tool_calls = self._parse_alternative_tool_calls(response)
|
|
909
|
+
|
|
910
|
+
if not matches and not alt_tool_calls:
|
|
911
|
+
# No tools found, just yield the response
|
|
912
|
+
yield response
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
# Yield any text BEFORE the first tool call immediately
|
|
916
|
+
first_tool_pos = response.find("```json")
|
|
917
|
+
if first_tool_pos > 0:
|
|
918
|
+
text_before_tools = response[:first_tool_pos].strip()
|
|
919
|
+
if text_before_tools:
|
|
920
|
+
yield text_before_tools + "\n"
|
|
921
|
+
|
|
922
|
+
# Process all tool calls first, collect results
|
|
923
|
+
tool_results = []
|
|
924
|
+
should_end_response = False
|
|
925
|
+
|
|
926
|
+
# Combine standard JSON matches and alternative format tool calls
|
|
927
|
+
all_tool_calls = []
|
|
928
|
+
|
|
929
|
+
# Parse standard JSON format
|
|
930
|
+
for match in matches:
|
|
931
|
+
try:
|
|
932
|
+
tool_call = json.loads(match.strip())
|
|
933
|
+
all_tool_calls.append(tool_call)
|
|
934
|
+
except json.JSONDecodeError:
|
|
935
|
+
continue
|
|
936
|
+
|
|
937
|
+
# Add alternative format tool calls
|
|
938
|
+
all_tool_calls.extend(alt_tool_calls)
|
|
939
|
+
|
|
940
|
+
for tool_call in all_tool_calls:
|
|
941
|
+
try:
|
|
942
|
+
tool_name = tool_call.get("tool_code")
|
|
943
|
+
args = tool_call.get("args", {})
|
|
944
|
+
|
|
945
|
+
# Check for response control tool
|
|
946
|
+
if tool_name == "response_control":
|
|
947
|
+
operation = args.get("operation", "end_response")
|
|
948
|
+
if operation == "end_response":
|
|
949
|
+
should_end_response = True
|
|
950
|
+
tool_results.append(
|
|
951
|
+
{
|
|
952
|
+
"type": "control",
|
|
953
|
+
"display": f"\n✅ **Response Completed**\n",
|
|
954
|
+
}
|
|
955
|
+
)
|
|
956
|
+
continue
|
|
957
|
+
|
|
958
|
+
# Execute the tool
|
|
959
|
+
if tool_name == "command_runner":
|
|
960
|
+
cmd_args = args.copy()
|
|
961
|
+
if "operation" not in cmd_args:
|
|
962
|
+
cmd_args["operation"] = "run_command"
|
|
963
|
+
if "cwd" not in cmd_args:
|
|
964
|
+
cmd_args["cwd"] = project_path or "."
|
|
965
|
+
|
|
966
|
+
result = await self.tool_registry.execute_tool(
|
|
967
|
+
"command_runner", user_id="ai_engine", **cmd_args
|
|
968
|
+
)
|
|
969
|
+
if result.success:
|
|
970
|
+
command = args.get("command", "unknown")
|
|
971
|
+
stdout = (
|
|
972
|
+
result.data.get("stdout", "")
|
|
973
|
+
if isinstance(result.data, dict)
|
|
974
|
+
else str(result.data)
|
|
975
|
+
)
|
|
976
|
+
stderr = result.data.get("stderr", "") if isinstance(result.data, dict) else ""
|
|
977
|
+
|
|
978
|
+
# Combine stdout and stderr for full output
|
|
979
|
+
full_output = stdout
|
|
980
|
+
if stderr and stderr.strip():
|
|
981
|
+
full_output += f"\n[stderr]\n{stderr}"
|
|
982
|
+
|
|
983
|
+
# Truncate very long outputs for display (but keep full output in data)
|
|
984
|
+
display_output = full_output
|
|
985
|
+
if len(full_output) > 10000:
|
|
986
|
+
display_output = full_output[:10000] + f"\n\n... [Output truncated - {len(full_output)} total characters]"
|
|
987
|
+
|
|
988
|
+
tool_results.append(
|
|
989
|
+
{
|
|
990
|
+
"type": "command",
|
|
991
|
+
"command": command,
|
|
992
|
+
"output": full_output,
|
|
993
|
+
"display": f"\n✅ **Tool Used:** command_runner\n⚡ **Command:** {command}\n📄 **Output:**\n```\n{display_output}\n```\n",
|
|
994
|
+
}
|
|
995
|
+
)
|
|
996
|
+
else:
|
|
997
|
+
tool_results.append(
|
|
998
|
+
{
|
|
999
|
+
"type": "error",
|
|
1000
|
+
"display": f"\n❌ **Command Error:** {result.error}\n",
|
|
1001
|
+
}
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
elif tool_name == "file_operations":
|
|
1005
|
+
# Prepend project_path to relative file paths
|
|
1006
|
+
file_path = args.get("file_path")
|
|
1007
|
+
if file_path and project_path:
|
|
1008
|
+
from pathlib import Path
|
|
1009
|
+
|
|
1010
|
+
path = Path(file_path)
|
|
1011
|
+
if not path.is_absolute():
|
|
1012
|
+
args["file_path"] = str(Path(project_path) / file_path)
|
|
1013
|
+
|
|
1014
|
+
result = await self.tool_registry.execute_tool(
|
|
1015
|
+
"file_operations", user_id="ai_engine", **args
|
|
1016
|
+
)
|
|
1017
|
+
if result.success:
|
|
1018
|
+
operation = args.get("operation")
|
|
1019
|
+
file_path = args.get("file_path")
|
|
1020
|
+
|
|
1021
|
+
if operation == "read_file" or operation == "read_file_lines":
|
|
1022
|
+
content = (
|
|
1023
|
+
result.data.get("content", "")
|
|
1024
|
+
if isinstance(result.data, dict)
|
|
1025
|
+
else str(result.data)
|
|
1026
|
+
)
|
|
1027
|
+
# Truncate very long content for display
|
|
1028
|
+
display_content = content
|
|
1029
|
+
if len(content) > 5000:
|
|
1030
|
+
display_content = (
|
|
1031
|
+
content[:5000]
|
|
1032
|
+
+ f"\n...[{len(content) - 5000} more characters truncated]"
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
# Add line info for read_file_lines
|
|
1036
|
+
line_info = ""
|
|
1037
|
+
if operation == "read_file_lines" and isinstance(
|
|
1038
|
+
result.data, dict
|
|
1039
|
+
):
|
|
1040
|
+
start = result.data.get("start_line", 1)
|
|
1041
|
+
end = result.data.get("end_line", 1)
|
|
1042
|
+
total = result.data.get("total_lines", 0)
|
|
1043
|
+
line_info = f" (lines {start}-{end} of {total})"
|
|
1044
|
+
|
|
1045
|
+
tool_results.append(
|
|
1046
|
+
{
|
|
1047
|
+
"type": "file_read",
|
|
1048
|
+
"file_path": file_path,
|
|
1049
|
+
"content": content, # Full content for AI processing
|
|
1050
|
+
"display": f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}{line_info}\n📄 **Result:**\n```\n{display_content}\n```\n",
|
|
1051
|
+
}
|
|
1052
|
+
)
|
|
1053
|
+
elif operation == "write_file_lines":
|
|
1054
|
+
# Show line range info for write_file_lines
|
|
1055
|
+
line_info = ""
|
|
1056
|
+
if isinstance(result.data, dict):
|
|
1057
|
+
start = result.data.get("start_line", 1)
|
|
1058
|
+
end = result.data.get("end_line", 1)
|
|
1059
|
+
lines_written = result.data.get("lines_written", 0)
|
|
1060
|
+
line_info = f" (lines {start}-{end}, {lines_written} lines written)"
|
|
1061
|
+
tool_results.append(
|
|
1062
|
+
{
|
|
1063
|
+
"type": "file_op",
|
|
1064
|
+
"display": f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}{line_info}\n",
|
|
1065
|
+
}
|
|
1066
|
+
)
|
|
1067
|
+
else:
|
|
1068
|
+
tool_results.append(
|
|
1069
|
+
{
|
|
1070
|
+
"type": "file_op",
|
|
1071
|
+
"display": f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}\n",
|
|
1072
|
+
}
|
|
1073
|
+
)
|
|
1074
|
+
else:
|
|
1075
|
+
tool_results.append(
|
|
1076
|
+
{
|
|
1077
|
+
"type": "error",
|
|
1078
|
+
"display": f"\n❌ **File Error:** {result.error}\n",
|
|
1079
|
+
}
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
elif tool_name == "web_search":
|
|
1083
|
+
operation = args.get("operation", "search_web")
|
|
1084
|
+
result = await self.tool_registry.execute_tool(
|
|
1085
|
+
"web_search", user_id="ai_engine", **args
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
if result.success:
|
|
1089
|
+
# Format the result based on operation type
|
|
1090
|
+
if operation == "search_web":
|
|
1091
|
+
query = args.get("query", "unknown")
|
|
1092
|
+
search_results = result.data if result.data else []
|
|
1093
|
+
result_text = f"\n✅ **Tool Used:** web_search\n🔍 **Query:** {query}\n\n**Search Results:**\n"
|
|
1094
|
+
for idx, item in enumerate(search_results[:5], 1):
|
|
1095
|
+
result_text += (
|
|
1096
|
+
f"\n{idx}. **{item.get('title', 'No title')}**\n"
|
|
1097
|
+
)
|
|
1098
|
+
result_text += (
|
|
1099
|
+
f" {item.get('snippet', 'No description')}\n"
|
|
1100
|
+
)
|
|
1101
|
+
result_text += f" 🔗 {item.get('url', 'No URL')}\n"
|
|
1102
|
+
|
|
1103
|
+
tool_results.append(
|
|
1104
|
+
{
|
|
1105
|
+
"type": "web_search",
|
|
1106
|
+
"query": query,
|
|
1107
|
+
"results": search_results,
|
|
1108
|
+
"display": result_text,
|
|
1109
|
+
}
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
elif operation == "fetch_url_content":
|
|
1113
|
+
url = args.get("url", "unknown")
|
|
1114
|
+
content_data = result.data if result.data else {}
|
|
1115
|
+
title = content_data.get("title", "No title")
|
|
1116
|
+
content = content_data.get("content", "No content")
|
|
1117
|
+
content_type = content_data.get("content_type", "text")
|
|
1118
|
+
|
|
1119
|
+
# Truncate content if too long for display
|
|
1120
|
+
display_content = content
|
|
1121
|
+
max_display = 2000
|
|
1122
|
+
if len(content) > max_display:
|
|
1123
|
+
display_content = (
|
|
1124
|
+
content[:max_display]
|
|
1125
|
+
+ f"\n\n... (truncated, total length: {len(content)} characters)"
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
result_text = (
|
|
1129
|
+
f"\n✅ **Tool Used:** web_search (fetch_url_content)\n"
|
|
1130
|
+
)
|
|
1131
|
+
result_text += f"🌐 **URL:** {url}\n"
|
|
1132
|
+
result_text += f"📄 **Title:** {title}\n"
|
|
1133
|
+
result_text += f"📝 **Content Type:** {content_type}\n\n"
|
|
1134
|
+
result_text += (
|
|
1135
|
+
f"**Content:**\n```\n{display_content}\n```\n"
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
tool_results.append(
|
|
1139
|
+
{
|
|
1140
|
+
"type": "web_fetch",
|
|
1141
|
+
"url": url,
|
|
1142
|
+
"content": content, # Full content for AI processing
|
|
1143
|
+
"display": result_text,
|
|
1144
|
+
}
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
elif operation == "parse_documentation":
|
|
1148
|
+
url = args.get("url", "unknown")
|
|
1149
|
+
doc_data = result.data if result.data else {}
|
|
1150
|
+
result_text = f"\n✅ **Tool Used:** web_search (parse_documentation)\n"
|
|
1151
|
+
result_text += f"🌐 **URL:** {url}\n"
|
|
1152
|
+
result_text += (
|
|
1153
|
+
f"📄 **Title:** {doc_data.get('title', 'No title')}\n"
|
|
1154
|
+
)
|
|
1155
|
+
result_text += f"📚 **Type:** {doc_data.get('doc_type', 'unknown')}\n\n"
|
|
1156
|
+
|
|
1157
|
+
sections = doc_data.get("sections", [])
|
|
1158
|
+
if sections:
|
|
1159
|
+
result_text += "**Sections:**\n"
|
|
1160
|
+
for section in sections[:5]:
|
|
1161
|
+
result_text += (
|
|
1162
|
+
f"\n• {section.get('title', 'Untitled')}\n"
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
tool_results.append(
|
|
1166
|
+
{
|
|
1167
|
+
"type": "web_docs",
|
|
1168
|
+
"url": url,
|
|
1169
|
+
"doc_data": doc_data,
|
|
1170
|
+
"display": result_text,
|
|
1171
|
+
}
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
elif operation == "get_api_docs":
|
|
1175
|
+
api_name = args.get("api_name", "unknown")
|
|
1176
|
+
api_data = result.data if result.data else {}
|
|
1177
|
+
if api_data.get("found", True):
|
|
1178
|
+
result_text = (
|
|
1179
|
+
f"\n✅ **Tool Used:** web_search (get_api_docs)\n"
|
|
1180
|
+
)
|
|
1181
|
+
result_text += f"📚 **API:** {api_name}\n"
|
|
1182
|
+
result_text += f"📄 **Title:** {api_data.get('title', 'API Documentation')}\n"
|
|
1183
|
+
else:
|
|
1184
|
+
result_text = (
|
|
1185
|
+
f"\n⚠️ **Tool Used:** web_search (get_api_docs)\n"
|
|
1186
|
+
)
|
|
1187
|
+
result_text += f"📚 **API:** {api_name}\n"
|
|
1188
|
+
result_text += f"❌ {api_data.get('message', 'Documentation not found')}\n"
|
|
1189
|
+
|
|
1190
|
+
tool_results.append(
|
|
1191
|
+
{
|
|
1192
|
+
"type": "web_api_docs",
|
|
1193
|
+
"api_name": api_name,
|
|
1194
|
+
"api_data": api_data,
|
|
1195
|
+
"display": result_text,
|
|
1196
|
+
}
|
|
1197
|
+
)
|
|
1198
|
+
else:
|
|
1199
|
+
tool_results.append(
|
|
1200
|
+
{
|
|
1201
|
+
"type": "web_search",
|
|
1202
|
+
"display": f"\n✅ **Tool Used:** web_search ({operation})\n",
|
|
1203
|
+
}
|
|
1204
|
+
)
|
|
1205
|
+
else:
|
|
1206
|
+
tool_results.append(
|
|
1207
|
+
{
|
|
1208
|
+
"type": "error",
|
|
1209
|
+
"display": f"\n❌ **Web Search Error:** {result.error}\n",
|
|
1210
|
+
}
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
except json.JSONDecodeError as e:
|
|
1214
|
+
tool_results.append(
|
|
1215
|
+
{
|
|
1216
|
+
"type": "error",
|
|
1217
|
+
"display": f"\n❌ **JSON Parse Error:** {str(e)}\n",
|
|
1218
|
+
}
|
|
1219
|
+
)
|
|
1220
|
+
except Exception as e:
|
|
1221
|
+
tool_results.append(
|
|
1222
|
+
{"type": "error", "display": f"\n❌ **Tool Error:** {str(e)}\n"}
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
# Now yield the tool results
|
|
1226
|
+
if tool_results:
|
|
1227
|
+
# Show all tool results
|
|
1228
|
+
for tool_result in tool_results:
|
|
1229
|
+
yield tool_result["display"]
|
|
1230
|
+
|
|
1231
|
+
# Check if we should end the response (end_response tool was called)
|
|
1232
|
+
if should_end_response:
|
|
1233
|
+
return
|
|
1234
|
+
|
|
1235
|
+
# Use auto-continuation manager to determine if we should continue
|
|
1236
|
+
if self.auto_continuation.should_continue(tool_results, should_end_response) and recursion_depth < MAX_RECURSION_DEPTH:
|
|
1237
|
+
# Show continuation indicator
|
|
1238
|
+
yield "\n🔄 **Auto-continuing...**\n"
|
|
1239
|
+
|
|
1240
|
+
# Generate continuation response using auto-continuation manager
|
|
1241
|
+
final_response = await self.auto_continuation.generate_continuation(
|
|
1242
|
+
ai_provider=ai_provider,
|
|
1243
|
+
messages=messages,
|
|
1244
|
+
tool_results=tool_results,
|
|
1245
|
+
model=model,
|
|
1246
|
+
config=config
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
# Clean model-specific syntax from continuation response
|
|
1250
|
+
final_response = self._clean_model_syntax(final_response)
|
|
1251
|
+
|
|
1252
|
+
# Check if continuation response contains tool calls
|
|
1253
|
+
if self._contains_tool_calls(final_response):
|
|
1254
|
+
# Extract and yield any text before the first tool call
|
|
1255
|
+
tool_call_start = final_response.find('```json')
|
|
1256
|
+
if tool_call_start > 0:
|
|
1257
|
+
text_before_tools = final_response[:tool_call_start].strip()
|
|
1258
|
+
if text_before_tools:
|
|
1259
|
+
yield f"\n{text_before_tools}\n"
|
|
1260
|
+
|
|
1261
|
+
# Recursively process the continuation response with tools
|
|
1262
|
+
async for chunk in self._process_response_with_tools(
|
|
1263
|
+
final_response,
|
|
1264
|
+
project_path,
|
|
1265
|
+
messages,
|
|
1266
|
+
ai_provider,
|
|
1267
|
+
model,
|
|
1268
|
+
config,
|
|
1269
|
+
recursion_depth + 1,
|
|
1270
|
+
):
|
|
1271
|
+
yield chunk
|
|
1272
|
+
else:
|
|
1273
|
+
# No tool calls in continuation, just yield the response
|
|
1274
|
+
yield f"\n{final_response}"
|
|
1275
|
+
else:
|
|
1276
|
+
# No tools, just yield the original response
|
|
1277
|
+
yield response
|
|
1278
|
+
|
|
1279
|
+
async def _process_tool_calls_stream(
|
|
1280
|
+
self, response_text: str, project_path: str = None
|
|
1281
|
+
):
|
|
1282
|
+
"""Process tool calls from response text and yield results"""
|
|
1283
|
+
import re
|
|
1284
|
+
|
|
1285
|
+
# Find all JSON blocks in the response
|
|
1286
|
+
json_pattern = r"```json\s*\n(.*?)\n```"
|
|
1287
|
+
matches = re.findall(json_pattern, response_text, re.DOTALL)
|
|
1288
|
+
|
|
1289
|
+
for match in matches:
|
|
1290
|
+
try:
|
|
1291
|
+
tool_call = json.loads(match.strip())
|
|
1292
|
+
tool_name = tool_call.get("tool_code")
|
|
1293
|
+
args = tool_call.get("args", {})
|
|
1294
|
+
|
|
1295
|
+
if tool_name == "command_runner":
|
|
1296
|
+
# Handle command runner
|
|
1297
|
+
cmd_args = args.copy()
|
|
1298
|
+
if "operation" not in cmd_args:
|
|
1299
|
+
cmd_args["operation"] = "run_command"
|
|
1300
|
+
if "cwd" not in cmd_args:
|
|
1301
|
+
cmd_args["cwd"] = project_path or "."
|
|
1302
|
+
|
|
1303
|
+
result = await self.tool_registry.execute_tool(
|
|
1304
|
+
"command_runner", user_id="ai_engine", **cmd_args
|
|
1305
|
+
)
|
|
1306
|
+
if result.success:
|
|
1307
|
+
command = args.get("command", "unknown")
|
|
1308
|
+
output = (
|
|
1309
|
+
result.data.get("stdout", "")
|
|
1310
|
+
if isinstance(result.data, dict)
|
|
1311
|
+
else str(result.data)
|
|
1312
|
+
)
|
|
1313
|
+
yield f"\n✅ **Tool Used:** command_runner\n⚡ **Command:** {command}\n📄 **Output:**\n```\n{output}\n```\n"
|
|
1314
|
+
else:
|
|
1315
|
+
yield f"\n❌ **Command Error:** {result.error}\n"
|
|
1316
|
+
|
|
1317
|
+
elif tool_name == "file_operations":
|
|
1318
|
+
# Handle file operations
|
|
1319
|
+
operation = args.get("operation")
|
|
1320
|
+
file_path = args.get("file_path")
|
|
1321
|
+
|
|
1322
|
+
# Prepend project_path to relative file paths
|
|
1323
|
+
if file_path and project_path:
|
|
1324
|
+
from pathlib import Path
|
|
1325
|
+
|
|
1326
|
+
path = Path(file_path)
|
|
1327
|
+
if not path.is_absolute():
|
|
1328
|
+
args["file_path"] = str(Path(project_path) / file_path)
|
|
1329
|
+
file_path = args["file_path"]
|
|
1330
|
+
|
|
1331
|
+
result = await self.tool_registry.execute_tool(
|
|
1332
|
+
"file_operations", user_id="ai_engine", **args
|
|
1333
|
+
)
|
|
1334
|
+
if result.success:
|
|
1335
|
+
if operation == "read_file":
|
|
1336
|
+
content = (
|
|
1337
|
+
result.data.get("content", "")
|
|
1338
|
+
if isinstance(result.data, dict)
|
|
1339
|
+
else str(result.data)
|
|
1340
|
+
)
|
|
1341
|
+
yield f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}\n📄 **Result:**\n```\n{content}\n```\n"
|
|
1342
|
+
else:
|
|
1343
|
+
yield f"\n✅ **Tool Used:** file_operations\n📄 **Operation:** {operation} on {file_path}\n"
|
|
1344
|
+
else:
|
|
1345
|
+
yield f"\n❌ **File Error:** {result.error}\n"
|
|
1346
|
+
|
|
1347
|
+
except json.JSONDecodeError as e:
|
|
1348
|
+
yield f"\n❌ **JSON Parse Error:** {str(e)}\n"
|
|
1349
|
+
except Exception as e:
|
|
1350
|
+
yield f"\n❌ **Tool Error:** {str(e)}\n"
|
|
1351
|
+
|
|
1352
|
+
def _build_system_prompt(self, project_path: str = None) -> str:
|
|
1353
|
+
"""Build system prompt for the AI"""
|
|
1354
|
+
|
|
1355
|
+
# Load user-defined rules FIRST (highest priority)
|
|
1356
|
+
from .rules import RulesManager
|
|
1357
|
+
rules_manager = RulesManager()
|
|
1358
|
+
rules_text = rules_manager.get_rules_for_ai(project_path)
|
|
1359
|
+
|
|
1360
|
+
prompt = ""
|
|
1361
|
+
|
|
1362
|
+
# Add rules at the very beginning if they exist
|
|
1363
|
+
if rules_text:
|
|
1364
|
+
prompt += "="*80 + "\n"
|
|
1365
|
+
prompt += "USER-DEFINED RULES (HIGHEST PRIORITY - MUST FOLLOW ABOVE ALL ELSE):\n"
|
|
1366
|
+
prompt += "="*80 + "\n"
|
|
1367
|
+
prompt += rules_text + "\n"
|
|
1368
|
+
prompt += "="*80 + "\n"
|
|
1369
|
+
prompt += "These rules OVERRIDE all other instructions. Follow them strictly.\n"
|
|
1370
|
+
prompt += "="*80 + "\n\n"
|
|
1371
|
+
|
|
1372
|
+
prompt += """You are Cognautic, an advanced AI coding assistant running inside the Cognautic CLI.
|
|
1373
|
+
|
|
1374
|
+
IMPORTANT: You are operating within the Cognautic CLI environment. You can ONLY use the tools provided below. Do NOT suggest using external tools, IDEs, or commands that are not available in this CLI.
|
|
1375
|
+
|
|
1376
|
+
Most Important Instruction:
|
|
1377
|
+
If the project is looking HARD, then perform a web search about the project or topic you’re going to work on.
|
|
1378
|
+
|
|
1379
|
+
Your capabilities within Cognautic CLI:
|
|
1380
|
+
1. Code analysis and review
|
|
1381
|
+
2. Project building and scaffolding
|
|
1382
|
+
3. Debugging and troubleshooting
|
|
1383
|
+
4. Documentation generation
|
|
1384
|
+
5. Best practices and optimization
|
|
1385
|
+
|
|
1386
|
+
CRITICAL BEHAVIOR REQUIREMENTS:
|
|
1387
|
+
- COMPLETE ENTIRE REQUESTS IN ONE RESPONSE: When a user asks you to build, create, or develop something, you must complete the ENTIRE task in a single response, not just one step at a time.
|
|
1388
|
+
- CREATE ALL NECESSARY FILES: If building a project (like a Pomodoro clock, web app, etc.), create ALL required files (HTML, CSS, JavaScript, etc.) in one go.
|
|
1389
|
+
- PROVIDE COMPREHENSIVE SOLUTIONS: Don't stop after creating just one file - complete the entire functional project.
|
|
1390
|
+
- BE PROACTIVE: Anticipate what files and functionality are needed and create them all without asking for permission for each step.
|
|
1391
|
+
- EXPLORATION IS OPTIONAL: You may explore the workspace with 'ls' or 'pwd' if needed, but this is NOT required before creating new files. If the user asks you to BUILD or CREATE something, prioritize creating the files immediately.
|
|
1392
|
+
- ALWAYS USE end_response TOOL: When you have completed ALL tasks, ALWAYS call the end_response tool to prevent unnecessary continuation
|
|
1393
|
+
- NEVER RE-READ SAME FILE: If a file was truncated in the output, use read_file_lines to read the specific truncated section, DO NOT re-read the entire file
|
|
1394
|
+
|
|
1395
|
+
WORKSPACE EXPLORATION RULES (CRITICAL - ALWAYS CHECK FIRST):
|
|
1396
|
+
- ALWAYS start by listing directory contents to see what files exist in the current directory
|
|
1397
|
+
* On Linux/Mac: Use 'ls' or 'ls -la' for detailed listing
|
|
1398
|
+
* On Windows: Use 'dir' or 'dir /a' for detailed listing
|
|
1399
|
+
- NEVER assume a project doesn't exist - ALWAYS check first by listing directory
|
|
1400
|
+
- When user mentions a project/app name (e.g., "cymox", "app", etc.), assume it EXISTS and check for it
|
|
1401
|
+
- When asked to ADD/MODIFY features: FIRST list directory to find existing files, then read and modify them
|
|
1402
|
+
- When asked to BUILD/CREATE NEW projects from scratch: Create all necessary files
|
|
1403
|
+
- When asked to MODIFY existing files: FIRST check if they exist by listing directory, then read and modify them
|
|
1404
|
+
- If user mentions specific files or features, assume they're talking about an EXISTING project unless explicitly stated otherwise
|
|
1405
|
+
- For searching files in large projects:
|
|
1406
|
+
* On Linux/Mac: Use 'find' command
|
|
1407
|
+
* On Windows: Use 'dir /s' or 'where' command
|
|
1408
|
+
|
|
1409
|
+
UNDERSTANDING USER CONTEXT (CRITICAL):
|
|
1410
|
+
- When user mentions adding features to an app/project, they are referring to an EXISTING project
|
|
1411
|
+
- ALWAYS list directory first to understand the project structure before making changes
|
|
1412
|
+
* Linux/Mac: 'ls' or 'ls -la'
|
|
1413
|
+
* Windows: 'dir' or 'dir /a'
|
|
1414
|
+
- Read relevant files to understand the current implementation before modifying
|
|
1415
|
+
- DO NOT create standalone examples when user asks to modify existing projects
|
|
1416
|
+
- If user says "add X to Y", assume Y exists and find it first
|
|
1417
|
+
- Parse user requests carefully - vague requests like "add export button" mean modify existing code, not create new files
|
|
1418
|
+
- When user mentions specific UI elements (e.g., "properties panel"), search for them in existing files
|
|
1419
|
+
|
|
1420
|
+
IMPORTANT: You have access to tools that you MUST use when appropriate. Don't just provide code examples - actually create files and execute commands when the user asks for them.
|
|
1421
|
+
|
|
1422
|
+
TOOL USAGE RULES:
|
|
1423
|
+
- When a user asks you to "create", "build", "make" files or projects, you MUST use the file_operations tool to create ALL necessary files
|
|
1424
|
+
- CREATE EACH FILE SEPARATELY: Use one tool call per file - do NOT try to create multiple files in a single tool call
|
|
1425
|
+
- When you need to run commands, use the command_runner tool
|
|
1426
|
+
- When you need to search for information, use the web_search tool
|
|
1427
|
+
- Always use tools instead of just showing code examples
|
|
1428
|
+
- Use multiple tool calls in sequence to complete entire projects
|
|
1429
|
+
|
|
1430
|
+
WEB SEARCH TOOL USAGE (CRITICAL - WHEN TO USE):
|
|
1431
|
+
- ALWAYS use web_search when user asks to implement something that requires current/external information:
|
|
1432
|
+
* Latest API documentation (e.g., "implement OpenAI API", "use Stripe payment")
|
|
1433
|
+
* Current best practices or frameworks (e.g., "create a React app with latest features")
|
|
1434
|
+
* Libraries or packages that need version info (e.g., "use TailwindCSS", "implement chart.js")
|
|
1435
|
+
* Technologies you're not certain about or that may have changed
|
|
1436
|
+
* Any request mentioning "latest", "current", "modern", "up-to-date"
|
|
1437
|
+
- ALWAYS use web_search when user explicitly asks for research:
|
|
1438
|
+
* "Search for...", "Look up...", "Find information about..."
|
|
1439
|
+
* "What's the best way to...", "How do I...", "What are the options for..."
|
|
1440
|
+
- DO NOT use web_search for:
|
|
1441
|
+
* Basic programming concepts you already know
|
|
1442
|
+
* Simple file operations or code modifications
|
|
1443
|
+
* General coding tasks that don't require external information
|
|
1444
|
+
- When in doubt about implementation details, USE web_search to get accurate information
|
|
1445
|
+
|
|
1446
|
+
CRITICAL: NEVER DESCRIBE PLANS WITHOUT EXECUTING THEM
|
|
1447
|
+
- DO NOT say "I will create X, Y, and Z files" and then only create X
|
|
1448
|
+
- If you mention you will do something, you MUST include the tool calls to actually do it
|
|
1449
|
+
- Either execute ALL the tool calls you describe, or don't describe them at all
|
|
1450
|
+
- Keep explanatory text BRIEF - focus on executing tool calls
|
|
1451
|
+
- If you need to create 3 files, include ALL 3 tool calls in your response, not just 1
|
|
1452
|
+
|
|
1453
|
+
CRITICAL: COMPLETE MODIFICATION REQUESTS
|
|
1454
|
+
- When user asks to "make the UI dark/black themed" or "change X to Y", you MUST:
|
|
1455
|
+
1. Read the file (if needed to see current state)
|
|
1456
|
+
2. IMMEDIATELY write the modified version with the requested changes
|
|
1457
|
+
- DO NOT just read the file and describe what you see - MODIFY IT
|
|
1458
|
+
- DO NOT just explain what needs to be changed - ACTUALLY CHANGE IT
|
|
1459
|
+
- Reading without writing is INCOMPLETE - always follow read with write for modification requests
|
|
1460
|
+
|
|
1461
|
+
CRITICAL FILE OPERATION RULES:
|
|
1462
|
+
- When READING files: Check if they exist first with 'ls', then use read_file operation
|
|
1463
|
+
- When CREATING new projects: Immediately create all necessary files without exploration
|
|
1464
|
+
- When MODIFYING files: Check if they exist first, read them, then write changes
|
|
1465
|
+
- If a file doesn't exist when trying to read/modify it, inform the user and ask if they want to create it
|
|
1466
|
+
- Use create_file for new files, write_file for modifying existing files
|
|
1467
|
+
- For LARGE files (>10,000 lines), use read_file_lines and write_file_lines to work with specific sections
|
|
1468
|
+
- For PARTIAL file edits, use write_file_lines to replace specific line ranges without rewriting entire file
|
|
1469
|
+
|
|
1470
|
+
LINE-BASED FILE OPERATIONS (CRITICAL FOR LARGE FILES):
|
|
1471
|
+
- read_file_lines: Read specific lines from a file (useful for large files)
|
|
1472
|
+
- start_line: First line to read (1-indexed)
|
|
1473
|
+
- end_line: Last line to read (optional, defaults to end of file)
|
|
1474
|
+
- WHEN TO USE: If you see "...[X more characters truncated]" in file output, immediately use read_file_lines to read the truncated section
|
|
1475
|
+
- NEVER re-read the entire file if it was truncated - always use read_file_lines for the missing part
|
|
1476
|
+
- write_file_lines: Replace specific lines in a file (useful for partial edits)
|
|
1477
|
+
- start_line: First line to replace (1-indexed)
|
|
1478
|
+
- end_line: Last line to replace (optional, defaults to start_line)
|
|
1479
|
+
- content: New content to write
|
|
1480
|
+
- WHEN TO USE: For modifying specific sections of large files without rewriting the entire file
|
|
1481
|
+
|
|
1482
|
+
CRITICAL: NEVER PUT LARGE CONTENT IN JSON TOOL CALLS
|
|
1483
|
+
- If you need to write a file larger than 1000 lines, use write_file_lines to write it in sections
|
|
1484
|
+
- NEVER try to include entire large files in a single JSON tool call - it will FAIL
|
|
1485
|
+
- Break large file writes into multiple write_file_lines calls (e.g., lines 1-100, 101-200, etc.)
|
|
1486
|
+
- For small additions to existing files, use write_file_lines to insert/replace only the needed sections
|
|
1487
|
+
|
|
1488
|
+
IMPORTANT: To use tools, you MUST include JSON code blocks in this exact format:
|
|
1489
|
+
|
|
1490
|
+
```json
|
|
1491
|
+
{
|
|
1492
|
+
"tool_code": "file_operations",
|
|
1493
|
+
"args": {
|
|
1494
|
+
"operation": "create_file",
|
|
1495
|
+
"file_path": "index.html",
|
|
1496
|
+
"content": "<!DOCTYPE html>..."
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
ALTERNATIVE FORMATS (for models with special tokens):
|
|
1502
|
+
If your model uses special tokens, you can also use:
|
|
1503
|
+
- <|message|>{"command":"ls -la"}<|call|> for command_runner
|
|
1504
|
+
- <|message|>{"operation":"read_file","file_path":"app.js"}<|call|> for file_operations
|
|
1505
|
+
The system will automatically detect and parse these formats.
|
|
1506
|
+
|
|
1507
|
+
To read an existing file:
|
|
1508
|
+
|
|
1509
|
+
```json
|
|
1510
|
+
{
|
|
1511
|
+
"tool_code": "file_operations",
|
|
1512
|
+
"args": {
|
|
1513
|
+
"operation": "read_file",
|
|
1514
|
+
"file_path": "existing_file.txt"
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
```
|
|
1518
|
+
|
|
1519
|
+
To read specific lines from a large file:
|
|
1520
|
+
|
|
1521
|
+
```json
|
|
1522
|
+
{
|
|
1523
|
+
"tool_code": "file_operations",
|
|
1524
|
+
"args": {
|
|
1525
|
+
"operation": "read_file_lines",
|
|
1526
|
+
"file_path": "large_file.js",
|
|
1527
|
+
"start_line": 100,
|
|
1528
|
+
"end_line": 200
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
To replace specific lines in a file:
|
|
1534
|
+
|
|
1535
|
+
```json
|
|
1536
|
+
{
|
|
1537
|
+
"tool_code": "file_operations",
|
|
1538
|
+
"args": {
|
|
1539
|
+
"operation": "write_file_lines",
|
|
1540
|
+
"file_path": "app.js",
|
|
1541
|
+
"start_line": 50,
|
|
1542
|
+
"end_line": 75,
|
|
1543
|
+
"content": "// Updated code here\nfunction newFunction() {\n return true;\n}"
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
For multiple files, use separate tool calls:
|
|
1549
|
+
|
|
1550
|
+
```json
|
|
1551
|
+
{
|
|
1552
|
+
"tool_code": "file_operations",
|
|
1553
|
+
"args": {
|
|
1554
|
+
"operation": "create_file",
|
|
1555
|
+
"file_path": "style.css",
|
|
1556
|
+
"content": "body { margin: 0; }"
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
```
|
|
1560
|
+
|
|
1561
|
+
```json
|
|
1562
|
+
{
|
|
1563
|
+
"tool_code": "file_operations",
|
|
1564
|
+
"args": {
|
|
1565
|
+
"operation": "create_file",
|
|
1566
|
+
"file_path": "script.js",
|
|
1567
|
+
"content": "console.log('Hello');"
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
For commands (always explore first):
|
|
1573
|
+
|
|
1574
|
+
```json
|
|
1575
|
+
{
|
|
1576
|
+
"tool_code": "command_runner",
|
|
1577
|
+
"args": {
|
|
1578
|
+
"command": "ls -la"
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
```json
|
|
1584
|
+
{
|
|
1585
|
+
"tool_code": "command_runner",
|
|
1586
|
+
"args": {
|
|
1587
|
+
"command": "pwd"
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
```
|
|
1591
|
+
|
|
1592
|
+
For web search (when you need external information):
|
|
1593
|
+
|
|
1594
|
+
```json
|
|
1595
|
+
{
|
|
1596
|
+
"tool_code": "web_search",
|
|
1597
|
+
"args": {
|
|
1598
|
+
"operation": "search_web",
|
|
1599
|
+
"query": "OpenAI API latest documentation",
|
|
1600
|
+
"num_results": 5
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
```json
|
|
1606
|
+
{
|
|
1607
|
+
"tool_code": "web_search",
|
|
1608
|
+
"args": {
|
|
1609
|
+
"operation": "fetch_url_content",
|
|
1610
|
+
"url": "https://example.com/api/docs",
|
|
1611
|
+
"extract_text": true
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
```json
|
|
1617
|
+
{
|
|
1618
|
+
"tool_code": "web_search",
|
|
1619
|
+
"args": {
|
|
1620
|
+
"operation": "get_api_docs",
|
|
1621
|
+
"api_name": "openai",
|
|
1622
|
+
"version": "latest"
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
```
|
|
1626
|
+
|
|
1627
|
+
EXAMPLE WORKFLOWS:
|
|
1628
|
+
|
|
1629
|
+
When user asks to ADD FEATURE to existing project (e.g., "add export button to cymox"):
|
|
1630
|
+
1. FIRST: List directory to see what files exist (use 'ls' on Linux/Mac or 'dir' on Windows)
|
|
1631
|
+
2. SECOND: Read relevant files to understand current structure
|
|
1632
|
+
- If file is truncated, use read_file_lines to read the truncated section
|
|
1633
|
+
3. THIRD: Modify the appropriate files to add the feature
|
|
1634
|
+
- For SMALL changes (adding a section): Use write_file_lines to insert/modify only needed lines
|
|
1635
|
+
- For LARGE files: NEVER use write_file with entire content - use write_file_lines in sections
|
|
1636
|
+
4. FOURTH: Call end_response when done
|
|
1637
|
+
5. DO NOT create new standalone files - modify existing ones!
|
|
1638
|
+
|
|
1639
|
+
When user asks to BUILD NEW web interface from scratch:
|
|
1640
|
+
1. Immediately create ALL necessary files (index.html, style.css, script.js) with complete, working code
|
|
1641
|
+
2. Include ALL tool calls in your response
|
|
1642
|
+
3. Call end_response when done
|
|
1643
|
+
|
|
1644
|
+
When user asks to MODIFY a file (e.g., "make the UI black themed"):
|
|
1645
|
+
1. FIRST: Check if file exists by listing directory (if not already known)
|
|
1646
|
+
2. SECOND: Read the file to see current content
|
|
1647
|
+
3. THIRD: Write the modified version with requested changes
|
|
1648
|
+
4. FOURTH: Call end_response when done
|
|
1649
|
+
5. Do NOT just describe what you see - MODIFY IT
|
|
1650
|
+
|
|
1651
|
+
When user asks to READ/ANALYZE a file:
|
|
1652
|
+
1. First: List directory to see what files exist (use 'ls' on Linux/Mac or 'dir' on Windows)
|
|
1653
|
+
2. Then: If file exists, read it with file_operations
|
|
1654
|
+
3. Finally: Provide analysis based on actual file content
|
|
1655
|
+
4. Call end_response when done
|
|
1656
|
+
|
|
1657
|
+
When user asks to IMPLEMENT something requiring external/current information:
|
|
1658
|
+
1. FIRST: Use web_search to get latest documentation/information
|
|
1659
|
+
- Example: "implement Stripe payment" → search for "Stripe API latest documentation"
|
|
1660
|
+
- Example: "use TailwindCSS" → search for "TailwindCSS installation guide"
|
|
1661
|
+
2. SECOND: Review search results and fetch detailed content if needed
|
|
1662
|
+
3. THIRD: Create/modify files based on the researched information
|
|
1663
|
+
4. FOURTH: Call end_response when done
|
|
1664
|
+
5. DO NOT guess API endpoints or library usage - ALWAYS search first!
|
|
1665
|
+
|
|
1666
|
+
When user explicitly asks for RESEARCH:
|
|
1667
|
+
1. FIRST: Use web_search with appropriate query
|
|
1668
|
+
2. SECOND: Present search results to user
|
|
1669
|
+
3. THIRD: If user wants more details, fetch specific URLs
|
|
1670
|
+
4. FOURTH: Call end_response when done
|
|
1671
|
+
|
|
1672
|
+
The tools will execute automatically and show results. Keep explanatory text BRIEF.
|
|
1673
|
+
|
|
1674
|
+
Available tools:
|
|
1675
|
+
- file_operations: Create, read, write, delete files and directories
|
|
1676
|
+
- command_runner: Execute shell commands
|
|
1677
|
+
- web_search: Search the web for information
|
|
1678
|
+
- code_analysis: Analyze code files
|
|
1679
|
+
- response_control: Control response continuation (use end_response to stop auto-continuation)
|
|
1680
|
+
|
|
1681
|
+
RESPONSE CONTINUATION (CRITICAL - ALWAYS USE end_response):
|
|
1682
|
+
- By default, after executing tools, the AI will automatically continue to complete the task
|
|
1683
|
+
- YOU MUST ALWAYS use the response_control tool when you finish ALL work:
|
|
1684
|
+
```json
|
|
1685
|
+
{
|
|
1686
|
+
"tool_code": "response_control",
|
|
1687
|
+
"args": {
|
|
1688
|
+
"operation": "end_response"
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
```
|
|
1692
|
+
- WHEN TO USE end_response (ALWAYS use it in these cases):
|
|
1693
|
+
* After creating/modifying ALL requested files
|
|
1694
|
+
* After completing ALL steps of a multi-step task
|
|
1695
|
+
* After providing final explanation or summary
|
|
1696
|
+
* Basically: ALWAYS use it when you're done with everything
|
|
1697
|
+
- Do NOT use end_response ONLY if:
|
|
1698
|
+
* The task is incomplete
|
|
1699
|
+
* You're waiting for user input
|
|
1700
|
+
* You need to execute more tools
|
|
1701
|
+
- IMPORTANT: Forgetting to use end_response causes the user to manually type "continue" - ALWAYS use it!
|
|
1702
|
+
|
|
1703
|
+
REMEMBER:
|
|
1704
|
+
1. Use tools to actually perform actions, don't just provide code examples!
|
|
1705
|
+
2. Complete ENTIRE requests in ONE response - create all necessary files and functionality!
|
|
1706
|
+
3. Don't stop after one file - build complete, functional projects!
|
|
1707
|
+
4. NEVER promise to do something without including the tool calls to actually do it!
|
|
1708
|
+
5. For very long file content, the system will automatically handle it - just provide the full content
|
|
1709
|
+
|
|
1710
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1711
|
+
🎯 PROJECT COMPLETION CHECKLIST - MANDATORY FOR ALL PROJECT REQUESTS
|
|
1712
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1713
|
+
|
|
1714
|
+
When a user asks you to "build/create a [project type] app/project", you MUST complete ALL of these steps:
|
|
1715
|
+
|
|
1716
|
+
✅ STEP 1: Research (if needed)
|
|
1717
|
+
- Perform web search for best practices and current versions
|
|
1718
|
+
- Create documentation in MD/ folder with findings
|
|
1719
|
+
|
|
1720
|
+
✅ STEP 2: Create ALL Project Files
|
|
1721
|
+
- Create directory structure
|
|
1722
|
+
- Create ALL source files (components, styles, logic, etc.)
|
|
1723
|
+
- Create configuration files
|
|
1724
|
+
|
|
1725
|
+
✅ STEP 3: Create package.json/requirements.txt (CRITICAL - DON'T SKIP!)
|
|
1726
|
+
For JavaScript/Node.js/React projects:
|
|
1727
|
+
- Create package.json with correct dependencies and versions
|
|
1728
|
+
- Include proper scripts (start, build, test, dev)
|
|
1729
|
+
- Add project metadata (name, version, description)
|
|
1730
|
+
|
|
1731
|
+
For Python projects:
|
|
1732
|
+
- Create requirements.txt with all dependencies
|
|
1733
|
+
- Include version constraints where appropriate
|
|
1734
|
+
|
|
1735
|
+
✅ STEP 4: Install Dependencies (CRITICAL - DON'T SKIP!)
|
|
1736
|
+
- Tell the user to run npm install (or yarn install) for JavaScript projects
|
|
1737
|
+
- Tell the user to run pip install -r requirements.txt for Python projects
|
|
1738
|
+
|
|
1739
|
+
✅ STEP 5: Explanation
|
|
1740
|
+
- Provide clear instructions to user
|
|
1741
|
+
|
|
1742
|
+
✅ STEP 6: Final Summary
|
|
1743
|
+
- List all files created
|
|
1744
|
+
- Show how to run the project
|
|
1745
|
+
- Mention any next steps or customization options
|
|
1746
|
+
|
|
1747
|
+
⚠️ CRITICAL RULES:
|
|
1748
|
+
- DO NOT stop after creating source files - you MUST create package.json/requirements.txt!
|
|
1749
|
+
- DO NOT skip steps - complete the ENTIRE project setup!
|
|
1750
|
+
- DO NOT use end_response until ALL steps above are completed!
|
|
1751
|
+
- ALWAYS use end_response when done - NEVER leave the response hanging!
|
|
1752
|
+
|
|
1753
|
+
📋 EXAMPLE COMPLETE FLOW FOR REACT APP:
|
|
1754
|
+
1. Web search for React best practices and current versions
|
|
1755
|
+
2. Create MD/ documentation files with findings
|
|
1756
|
+
3. Create public/index.html
|
|
1757
|
+
4. Create src/ directory with all components (App.js, index.js, etc.)
|
|
1758
|
+
5. Create src/index.css and component styles
|
|
1759
|
+
6. Create package.json with dependencies ← DON'T SKIP THIS!
|
|
1760
|
+
8. Provide instructions: npm install and npm start
|
|
1761
|
+
9. THEN use end_response
|
|
1762
|
+
|
|
1763
|
+
If you find yourself about to use end_response, ask yourself:
|
|
1764
|
+
- Did I create package.json/requirements.txt? If NO → CREATE IT NOW!
|
|
1765
|
+
- Did I run npm install / pip install? If NO → RUN IT NOW!
|
|
1766
|
+
- Is the project ready to run? If NO → COMPLETE THE SETUP!
|
|
1767
|
+
|
|
1768
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1769
|
+
📦 COMMON PROJECT TYPES & REQUIRED FILES
|
|
1770
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1771
|
+
|
|
1772
|
+
REACT APP:
|
|
1773
|
+
✅ Must create: package.json, public/index.html, src/index.js, src/App.js, src/index.css
|
|
1774
|
+
✅ Must tell user to run: npm install
|
|
1775
|
+
✅ Dependencies: react, react-dom, react-scripts (check latest versions via web search)
|
|
1776
|
+
✅ Must tell user to run: npm start
|
|
1777
|
+
|
|
1778
|
+
NODE.JS/EXPRESS APP:
|
|
1779
|
+
✅ Must create: package.json, server.js (or index.js), .env template
|
|
1780
|
+
✅ Must tell user to run: npm install
|
|
1781
|
+
✅ Dependencies: express, and any other needed packages
|
|
1782
|
+
✅ Must tell user to run: node server.js or npm start
|
|
1783
|
+
|
|
1784
|
+
PYTHON APP:
|
|
1785
|
+
✅ Must create: requirements.txt, main.py (or app.py), README.md
|
|
1786
|
+
✅ Must tell user to run: pip install -r requirements.txt
|
|
1787
|
+
✅ Must tell user to run: python main.py
|
|
1788
|
+
|
|
1789
|
+
NEXT.JS APP:
|
|
1790
|
+
✅ Must create: package.json, pages/index.js, pages/_app.js, public/ folder
|
|
1791
|
+
✅ Must tell user to run: npm install
|
|
1792
|
+
✅ Dependencies: next, react, react-dom (check latest versions)
|
|
1793
|
+
✅ Must tell user to run: npm run dev
|
|
1794
|
+
|
|
1795
|
+
VITE + REACT APP:
|
|
1796
|
+
✅ Must create: package.json, index.html, src/main.jsx, src/App.jsx, vite.config.js
|
|
1797
|
+
✅ Must tell user to run: npm install
|
|
1798
|
+
✅ Dependencies: vite, react, react-dom, @vitejs/plugin-react
|
|
1799
|
+
✅ Must tell user to run: npm run dev
|
|
1800
|
+
|
|
1801
|
+
REMEMBER: The project is NOT complete until dependencies are installed and it's ready to run!
|
|
1802
|
+
|
|
1803
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1804
|
+
📊 SHOWING DIFFS AND FILE PREVIEWS
|
|
1805
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1806
|
+
|
|
1807
|
+
When creating or modifying files, provide helpful context:
|
|
1808
|
+
|
|
1809
|
+
FOR NEW FILES:
|
|
1810
|
+
- Show a preview of the file content (first 10-15 lines)
|
|
1811
|
+
- Indicate total line count
|
|
1812
|
+
- Mention key features or sections
|
|
1813
|
+
|
|
1814
|
+
FOR MODIFIED FILES:
|
|
1815
|
+
- Show what changed (before/after snippets)
|
|
1816
|
+
- Highlight the specific lines modified
|
|
1817
|
+
- Explain why the change was made
|
|
1818
|
+
|
|
1819
|
+
EXAMPLE OUTPUT FORMAT:
|
|
1820
|
+
✨ **File Created:** src/App.js
|
|
1821
|
+
📄 **Preview:**
|
|
1822
|
+
```javascript
|
|
1823
|
+
import React from 'react';
|
|
1824
|
+
import './App.css';
|
|
1825
|
+
|
|
1826
|
+
function App() {
|
|
1827
|
+
return (
|
|
1828
|
+
<div className="App">
|
|
1829
|
+
<h1>Welcome to My App</h1>
|
|
1830
|
+
</div>
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
... (15 more lines)
|
|
1834
|
+
```
|
|
1835
|
+
📊 Total: 23 lines
|
|
1836
|
+
|
|
1837
|
+
💾 **File Modified:** src/styles.css
|
|
1838
|
+
📝 **Changes:**
|
|
1839
|
+
- Line 5-8: Changed background color from white to dark theme
|
|
1840
|
+
- Line 12: Updated font size from 14px to 16px
|
|
1841
|
+
- Added: New dark mode variables (lines 20-25)
|
|
1842
|
+
|
|
1843
|
+
This helps users understand what you've done without overwhelming them with full file contents.
|
|
1844
|
+
"""
|
|
1845
|
+
|
|
1846
|
+
# Add OS information to help AI use correct commands
|
|
1847
|
+
import platform
|
|
1848
|
+
|
|
1849
|
+
os_name = platform.system()
|
|
1850
|
+
if os_name == "Windows":
|
|
1851
|
+
prompt += "\n\nOPERATING SYSTEM: Windows"
|
|
1852
|
+
prompt += "\nUse Windows commands: 'dir', 'dir /a', 'dir /s', 'where', etc."
|
|
1853
|
+
elif os_name == "Darwin":
|
|
1854
|
+
prompt += "\n\nOPERATING SYSTEM: macOS"
|
|
1855
|
+
prompt += "\nUse Unix commands: 'ls', 'ls -la', 'find', etc."
|
|
1856
|
+
else: # Linux and others
|
|
1857
|
+
prompt += "\n\nOPERATING SYSTEM: Linux"
|
|
1858
|
+
prompt += "\nUse Unix commands: 'ls', 'ls -la', 'find', etc."
|
|
1859
|
+
|
|
1860
|
+
if project_path:
|
|
1861
|
+
prompt += f"\n\nCurrent project path: {project_path}"
|
|
1862
|
+
prompt += "\nYou can analyze and modify files in this project."
|
|
1863
|
+
|
|
1864
|
+
return prompt
|
|
1865
|
+
|
|
1866
|
+
def _contains_tool_calls(self, response: str) -> bool:
|
|
1867
|
+
"""Check if response contains tool calls"""
|
|
1868
|
+
# Check for JSON tool call patterns
|
|
1869
|
+
import re
|
|
1870
|
+
|
|
1871
|
+
tool_patterns = [
|
|
1872
|
+
r'"tool_code":\s*"[^"]+?"',
|
|
1873
|
+
r'"tool_name":\s*"[^"]+?"',
|
|
1874
|
+
r"execute_command",
|
|
1875
|
+
r"command_runner",
|
|
1876
|
+
r"file_operations",
|
|
1877
|
+
r"web_search",
|
|
1878
|
+
r"code_analysis",
|
|
1879
|
+
]
|
|
1880
|
+
return any(re.search(pattern, response) for pattern in tool_patterns)
|
|
1881
|
+
|
|
1882
|
+
async def _execute_tools(self, response: str, project_path: str = None) -> str:
|
|
1883
|
+
"""Execute tools mentioned in the response"""
|
|
1884
|
+
import re
|
|
1885
|
+
import json
|
|
1886
|
+
|
|
1887
|
+
# Find JSON tool calls in the response
|
|
1888
|
+
json_pattern = r"```json\s*(\{[^`]+\})\s*```"
|
|
1889
|
+
matches = re.findall(json_pattern, response, re.DOTALL)
|
|
1890
|
+
|
|
1891
|
+
if not matches:
|
|
1892
|
+
return response
|
|
1893
|
+
|
|
1894
|
+
# Process each tool call
|
|
1895
|
+
results = []
|
|
1896
|
+
for match in matches:
|
|
1897
|
+
try:
|
|
1898
|
+
tool_call = json.loads(match)
|
|
1899
|
+
tool_name = tool_call.get("tool_code") or tool_call.get("tool_name")
|
|
1900
|
+
args = tool_call.get("args", {})
|
|
1901
|
+
|
|
1902
|
+
if tool_name in ["execute_command", "command_runner"]:
|
|
1903
|
+
command = args.get("command")
|
|
1904
|
+
if command:
|
|
1905
|
+
# Use command runner tool via registry
|
|
1906
|
+
cmd_args = args.copy()
|
|
1907
|
+
# Set default operation if not specified
|
|
1908
|
+
if "operation" not in cmd_args:
|
|
1909
|
+
cmd_args["operation"] = "run_command"
|
|
1910
|
+
if "cwd" not in cmd_args:
|
|
1911
|
+
cmd_args["cwd"] = project_path or "."
|
|
1912
|
+
result = await self.tool_registry.execute_tool(
|
|
1913
|
+
"command_runner", user_id="ai_engine", **cmd_args
|
|
1914
|
+
)
|
|
1915
|
+
if result.success:
|
|
1916
|
+
command = args.get("command", "unknown")
|
|
1917
|
+
results.append(
|
|
1918
|
+
f"✅ **Tool Used:** command_runner\n⚡ **Command Executed:** {command}"
|
|
1919
|
+
)
|
|
1920
|
+
else:
|
|
1921
|
+
results.append(
|
|
1922
|
+
f"❌ **Tool Error:** command_runner - {result.error}"
|
|
1923
|
+
)
|
|
1924
|
+
|
|
1925
|
+
elif tool_name == "file_operations":
|
|
1926
|
+
operation = args.get("operation")
|
|
1927
|
+
if operation:
|
|
1928
|
+
# Prepend project_path to relative file paths
|
|
1929
|
+
file_path = args.get("file_path")
|
|
1930
|
+
if file_path and project_path:
|
|
1931
|
+
from pathlib import Path
|
|
1932
|
+
|
|
1933
|
+
path = Path(file_path)
|
|
1934
|
+
if not path.is_absolute():
|
|
1935
|
+
args["file_path"] = str(Path(project_path) / file_path)
|
|
1936
|
+
|
|
1937
|
+
# Use file operations tool - pass all args directly
|
|
1938
|
+
result = await self.tool_registry.execute_tool(
|
|
1939
|
+
"file_operations", user_id="ai_engine", **args
|
|
1940
|
+
)
|
|
1941
|
+
if result.success:
|
|
1942
|
+
# Format file operation results concisely
|
|
1943
|
+
if operation == "create_file":
|
|
1944
|
+
file_path = args.get("file_path", "unknown")
|
|
1945
|
+
results.append(
|
|
1946
|
+
f"✅ **Tool Used:** file_operations\n📄 **File Created:** {file_path}"
|
|
1947
|
+
)
|
|
1948
|
+
elif operation == "write_file":
|
|
1949
|
+
file_path = args.get("file_path", "unknown")
|
|
1950
|
+
results.append(
|
|
1951
|
+
f"✅ **Tool Used:** file_operations\n📝 **File Edited:** {file_path}"
|
|
1952
|
+
)
|
|
1953
|
+
elif operation == "list_directory":
|
|
1954
|
+
dir_path = args.get("dir_path", "unknown")
|
|
1955
|
+
file_count = (
|
|
1956
|
+
len(result.data)
|
|
1957
|
+
if isinstance(result.data, list)
|
|
1958
|
+
else 0
|
|
1959
|
+
)
|
|
1960
|
+
results.append(
|
|
1961
|
+
f"✅ **Tool Used:** file_operations\n📁 **Directory Listed:** {dir_path} ({file_count} items)"
|
|
1962
|
+
)
|
|
1963
|
+
else:
|
|
1964
|
+
results.append(
|
|
1965
|
+
f"✅ **Tool Used:** file_operations ({operation})"
|
|
1966
|
+
)
|
|
1967
|
+
else:
|
|
1968
|
+
results.append(
|
|
1969
|
+
f"❌ **Tool Error:** file_operations - {result.error}"
|
|
1970
|
+
)
|
|
1971
|
+
|
|
1972
|
+
elif tool_name == "web_search":
|
|
1973
|
+
operation = args.get("operation", "search_web")
|
|
1974
|
+
# Use web search tool via registry
|
|
1975
|
+
result = await self.tool_registry.execute_tool(
|
|
1976
|
+
"web_search", user_id="ai_engine", **args
|
|
1977
|
+
)
|
|
1978
|
+
if result.success:
|
|
1979
|
+
# Format the result based on operation type
|
|
1980
|
+
if operation == "search_web":
|
|
1981
|
+
query = args.get("query", "unknown")
|
|
1982
|
+
search_results = result.data if result.data else []
|
|
1983
|
+
result_text = f"✅ **Tool Used:** web_search\n🔍 **Query:** {query}\n\n**Search Results:**\n"
|
|
1984
|
+
for idx, item in enumerate(search_results[:5], 1):
|
|
1985
|
+
result_text += (
|
|
1986
|
+
f"\n{idx}. **{item.get('title', 'No title')}**\n"
|
|
1987
|
+
)
|
|
1988
|
+
result_text += (
|
|
1989
|
+
f" {item.get('snippet', 'No description')}\n"
|
|
1990
|
+
)
|
|
1991
|
+
result_text += f" 🔗 {item.get('url', 'No URL')}\n"
|
|
1992
|
+
results.append(result_text)
|
|
1993
|
+
|
|
1994
|
+
elif operation == "fetch_url_content":
|
|
1995
|
+
url = args.get("url", "unknown")
|
|
1996
|
+
content_data = result.data if result.data else {}
|
|
1997
|
+
title = content_data.get("title", "No title")
|
|
1998
|
+
content = content_data.get("content", "No content")
|
|
1999
|
+
content_type = content_data.get("content_type", "text")
|
|
2000
|
+
|
|
2001
|
+
# Truncate content if too long
|
|
2002
|
+
max_display = 2000
|
|
2003
|
+
if len(content) > max_display:
|
|
2004
|
+
content = (
|
|
2005
|
+
content[:max_display]
|
|
2006
|
+
+ f"\n\n... (truncated, total length: {len(content)} characters)"
|
|
2007
|
+
)
|
|
2008
|
+
|
|
2009
|
+
result_text = (
|
|
2010
|
+
f"✅ **Tool Used:** web_search (fetch_url_content)\n"
|
|
2011
|
+
)
|
|
2012
|
+
result_text += f"🌐 **URL:** {url}\n"
|
|
2013
|
+
result_text += f"📄 **Title:** {title}\n"
|
|
2014
|
+
result_text += f"📝 **Content Type:** {content_type}\n\n"
|
|
2015
|
+
result_text += f"**Content:**\n{content}"
|
|
2016
|
+
results.append(result_text)
|
|
2017
|
+
|
|
2018
|
+
elif operation == "parse_documentation":
|
|
2019
|
+
url = args.get("url", "unknown")
|
|
2020
|
+
doc_data = result.data if result.data else {}
|
|
2021
|
+
result_text = (
|
|
2022
|
+
f"✅ **Tool Used:** web_search (parse_documentation)\n"
|
|
2023
|
+
)
|
|
2024
|
+
result_text += f"🌐 **URL:** {url}\n"
|
|
2025
|
+
result_text += (
|
|
2026
|
+
f"📄 **Title:** {doc_data.get('title', 'No title')}\n"
|
|
2027
|
+
)
|
|
2028
|
+
result_text += f"📚 **Type:** {doc_data.get('doc_type', 'unknown')}\n\n"
|
|
2029
|
+
|
|
2030
|
+
sections = doc_data.get("sections", [])
|
|
2031
|
+
if sections:
|
|
2032
|
+
result_text += "**Sections:**\n"
|
|
2033
|
+
for section in sections[:5]:
|
|
2034
|
+
result_text += (
|
|
2035
|
+
f"\n• {section.get('title', 'Untitled')}\n"
|
|
2036
|
+
)
|
|
2037
|
+
results.append(result_text)
|
|
2038
|
+
|
|
2039
|
+
elif operation == "get_api_docs":
|
|
2040
|
+
api_name = args.get("api_name", "unknown")
|
|
2041
|
+
api_data = result.data if result.data else {}
|
|
2042
|
+
if api_data.get("found", True):
|
|
2043
|
+
result_text = (
|
|
2044
|
+
f"✅ **Tool Used:** web_search (get_api_docs)\n"
|
|
2045
|
+
)
|
|
2046
|
+
result_text += f"📚 **API:** {api_name}\n"
|
|
2047
|
+
result_text += f"📄 **Title:** {api_data.get('title', 'API Documentation')}\n"
|
|
2048
|
+
results.append(result_text)
|
|
2049
|
+
else:
|
|
2050
|
+
result_text = (
|
|
2051
|
+
f"⚠️ **Tool Used:** web_search (get_api_docs)\n"
|
|
2052
|
+
)
|
|
2053
|
+
result_text += f"📚 **API:** {api_name}\n"
|
|
2054
|
+
result_text += f"❌ {api_data.get('message', 'Documentation not found')}\n"
|
|
2055
|
+
results.append(result_text)
|
|
2056
|
+
else:
|
|
2057
|
+
results.append(
|
|
2058
|
+
f"✅ **Tool Used:** web_search ({operation})"
|
|
2059
|
+
)
|
|
2060
|
+
else:
|
|
2061
|
+
results.append(
|
|
2062
|
+
f"❌ **Tool Error:** web_search - {result.error}"
|
|
2063
|
+
)
|
|
2064
|
+
|
|
2065
|
+
except Exception as e:
|
|
2066
|
+
results.append(f"**Tool Error:** {str(e)}")
|
|
2067
|
+
|
|
2068
|
+
# Replace the original response with just the tool results if tools were used
|
|
2069
|
+
if results:
|
|
2070
|
+
# Remove JSON tool calls from the response
|
|
2071
|
+
import re
|
|
2072
|
+
|
|
2073
|
+
# Remove JSON code blocks - more aggressive pattern
|
|
2074
|
+
response = re.sub(r"```json.*?```", "", response, flags=re.DOTALL)
|
|
2075
|
+
# Remove any remaining code blocks
|
|
2076
|
+
response = re.sub(r"```.*?```", "", response, flags=re.DOTALL)
|
|
2077
|
+
# Remove leftover JSON-like patterns
|
|
2078
|
+
response = re.sub(r'\{[\s\S]*?"tool_code"[\s\S]*?\}', "", response)
|
|
2079
|
+
# Clean up extra whitespace
|
|
2080
|
+
response = re.sub(r"\n\s*\n\s*\n+", "\n\n", response)
|
|
2081
|
+
response = response.strip()
|
|
2082
|
+
|
|
2083
|
+
# If response is mostly empty after removing code blocks, just show tool results
|
|
2084
|
+
if len(response.strip()) < 100:
|
|
2085
|
+
return "\n\n".join(results)
|
|
2086
|
+
else:
|
|
2087
|
+
return response + "\n\n" + "\n\n".join(results)
|
|
2088
|
+
|
|
2089
|
+
return response
|
|
2090
|
+
|
|
2091
|
+
async def build_project(
|
|
2092
|
+
self,
|
|
2093
|
+
description: str,
|
|
2094
|
+
language: str = None,
|
|
2095
|
+
framework: str = None,
|
|
2096
|
+
output_dir: str = None,
|
|
2097
|
+
interactive: bool = False,
|
|
2098
|
+
) -> Dict[str, Any]:
|
|
2099
|
+
"""Build a project based on description"""
|
|
2100
|
+
|
|
2101
|
+
prompt = f"""Build a {language or "appropriate"} project with the following description:
|
|
2102
|
+
{description}
|
|
2103
|
+
|
|
2104
|
+
Requirements:
|
|
2105
|
+
- Framework: {framework or "most suitable"}
|
|
2106
|
+
- Output directory: {output_dir or "current directory"}
|
|
2107
|
+
- Interactive mode: {interactive}
|
|
2108
|
+
|
|
2109
|
+
Please create a complete, working project structure with:
|
|
2110
|
+
1. Main application files
|
|
2111
|
+
2. Configuration files
|
|
2112
|
+
3. Dependencies/requirements
|
|
2113
|
+
4. README with setup instructions
|
|
2114
|
+
5. Basic tests if applicable
|
|
2115
|
+
|
|
2116
|
+
Provide step-by-step implementation."""
|
|
2117
|
+
|
|
2118
|
+
response = await self.process_message(prompt)
|
|
2119
|
+
|
|
2120
|
+
return {
|
|
2121
|
+
"status": "completed",
|
|
2122
|
+
"description": description,
|
|
2123
|
+
"output_path": output_dir or ".",
|
|
2124
|
+
"response": response,
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
async def analyze_project(
|
|
2128
|
+
self,
|
|
2129
|
+
project_path: str,
|
|
2130
|
+
output_format: str = "text",
|
|
2131
|
+
focus: str = None,
|
|
2132
|
+
include_suggestions: bool = False,
|
|
2133
|
+
) -> Any:
|
|
2134
|
+
"""Analyze a project and provide insights"""
|
|
2135
|
+
|
|
2136
|
+
project_path = Path(project_path)
|
|
2137
|
+
|
|
2138
|
+
# Gather project information
|
|
2139
|
+
project_info = self._gather_project_info(project_path)
|
|
2140
|
+
|
|
2141
|
+
prompt = f"""Analyze the following project:
|
|
2142
|
+
|
|
2143
|
+
Path: {project_path}
|
|
2144
|
+
Structure: {project_info["structure"]}
|
|
2145
|
+
Languages: {project_info["languages"]}
|
|
2146
|
+
Files: {len(project_info["files"])} files
|
|
2147
|
+
|
|
2148
|
+
Focus area: {focus or "general analysis"}
|
|
2149
|
+
Include suggestions: {include_suggestions}
|
|
2150
|
+
Output format: {output_format}
|
|
2151
|
+
|
|
2152
|
+
Provide a comprehensive analysis including:
|
|
2153
|
+
1. Project overview and architecture
|
|
2154
|
+
2. Code quality assessment
|
|
2155
|
+
3. Dependencies and security
|
|
2156
|
+
4. Performance considerations
|
|
2157
|
+
5. Best practices compliance
|
|
2158
|
+
"""
|
|
2159
|
+
|
|
2160
|
+
if include_suggestions:
|
|
2161
|
+
prompt += "\n6. Specific improvement suggestions"
|
|
2162
|
+
|
|
2163
|
+
response = await self.process_message(prompt, project_path=str(project_path))
|
|
2164
|
+
|
|
2165
|
+
if output_format == "json":
|
|
2166
|
+
# Try to structure the response as JSON
|
|
2167
|
+
try:
|
|
2168
|
+
return {
|
|
2169
|
+
"project_path": str(project_path),
|
|
2170
|
+
"analysis": response,
|
|
2171
|
+
"metadata": project_info,
|
|
2172
|
+
"timestamp": str(asyncio.get_event_loop().time()),
|
|
2173
|
+
}
|
|
2174
|
+
except Exception:
|
|
2175
|
+
return {"analysis": response, "error": "Could not structure as JSON"}
|
|
2176
|
+
|
|
2177
|
+
return response
|
|
2178
|
+
|
|
2179
|
+
def _gather_project_info(self, project_path: Path) -> Dict[str, Any]:
|
|
2180
|
+
"""Gather basic information about a project"""
|
|
2181
|
+
info = {"structure": [], "languages": set(), "files": []}
|
|
2182
|
+
|
|
2183
|
+
try:
|
|
2184
|
+
for item in project_path.rglob("*"):
|
|
2185
|
+
if item.is_file() and not any(
|
|
2186
|
+
part.startswith(".") for part in item.parts
|
|
2187
|
+
):
|
|
2188
|
+
relative_path = item.relative_to(project_path)
|
|
2189
|
+
info["files"].append(str(relative_path))
|
|
2190
|
+
|
|
2191
|
+
# Detect language by extension
|
|
2192
|
+
suffix = item.suffix.lower()
|
|
2193
|
+
language_map = {
|
|
2194
|
+
".py": "Python",
|
|
2195
|
+
".js": "JavaScript",
|
|
2196
|
+
".ts": "TypeScript",
|
|
2197
|
+
".java": "Java",
|
|
2198
|
+
".cpp": "C++",
|
|
2199
|
+
".c": "C",
|
|
2200
|
+
".go": "Go",
|
|
2201
|
+
".rs": "Rust",
|
|
2202
|
+
".php": "PHP",
|
|
2203
|
+
".rb": "Ruby",
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
if suffix in language_map:
|
|
2207
|
+
info["languages"].add(language_map[suffix])
|
|
2208
|
+
|
|
2209
|
+
except Exception as e:
|
|
2210
|
+
info["error"] = str(e)
|
|
2211
|
+
|
|
2212
|
+
info["languages"] = list(info["languages"])
|
|
2213
|
+
return info
|