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/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