codegraph-cli 2.1.0__py3-none-any.whl → 2.1.2__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.
Files changed (42) hide show
  1. codegraph_cli/__init__.py +1 -1
  2. codegraph_cli/agents.py +59 -3
  3. codegraph_cli/chat_agent.py +58 -11
  4. codegraph_cli/cli.py +569 -54
  5. codegraph_cli/cli_chat.py +204 -94
  6. codegraph_cli/cli_diagnose.py +13 -2
  7. codegraph_cli/cli_docs.py +207 -0
  8. codegraph_cli/cli_explore.py +1053 -0
  9. codegraph_cli/cli_export.py +941 -0
  10. codegraph_cli/cli_groups.py +33 -0
  11. codegraph_cli/cli_health.py +316 -0
  12. codegraph_cli/cli_history.py +213 -0
  13. codegraph_cli/cli_onboard.py +380 -0
  14. codegraph_cli/cli_quickstart.py +256 -0
  15. codegraph_cli/cli_refactor.py +17 -3
  16. codegraph_cli/cli_setup.py +12 -12
  17. codegraph_cli/cli_suggestions.py +90 -0
  18. codegraph_cli/cli_test.py +17 -3
  19. codegraph_cli/cli_tui.py +210 -0
  20. codegraph_cli/cli_v2.py +24 -4
  21. codegraph_cli/cli_watch.py +158 -0
  22. codegraph_cli/cli_workflows.py +255 -0
  23. codegraph_cli/codegen_agent.py +15 -1
  24. codegraph_cli/config.py +18 -5
  25. codegraph_cli/context_manager.py +117 -15
  26. codegraph_cli/crew_agents.py +32 -8
  27. codegraph_cli/crew_chat.py +146 -13
  28. codegraph_cli/crew_tools.py +30 -2
  29. codegraph_cli/embeddings.py +95 -5
  30. codegraph_cli/llm.py +42 -55
  31. codegraph_cli/project_context.py +64 -1
  32. codegraph_cli/rag.py +282 -19
  33. codegraph_cli/storage.py +310 -14
  34. codegraph_cli/vector_store.py +110 -8
  35. {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/METADATA +75 -21
  36. codegraph_cli-2.1.2.dist-info/RECORD +55 -0
  37. codegraph_cli-2.1.2.dist-info/entry_points.txt +2 -0
  38. codegraph_cli-2.1.0.dist-info/RECORD +0 -43
  39. codegraph_cli-2.1.0.dist-info/entry_points.txt +0 -2
  40. {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/WHEEL +0 -0
  41. {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/licenses/LICENSE +0 -0
  42. {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,255 @@
1
+ """Combined workflow commands for CodeGraph CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+
12
+ from . import config
13
+ from .diff_engine import DiffEngine
14
+ from .storage import GraphStore, ProjectManager
15
+
16
+ console = Console()
17
+
18
+ workflows_app = typer.Typer(help="🔄 Combined workflow commands")
19
+
20
+
21
+ def _ensure_project() -> tuple[ProjectManager, str]:
22
+ """Ensure a project is loaded and return (pm, project_name)."""
23
+ pm = ProjectManager()
24
+ project = pm.get_current_project()
25
+ if not project:
26
+ console.print("[red]✗[/red] No project loaded. Use 'cg load-project <name>' or run 'cg index <path>'.")
27
+ raise typer.Exit(1)
28
+ return pm, project
29
+
30
+
31
+ @workflows_app.command("review-and-fix")
32
+ def review_and_fix(
33
+ path: str = typer.Argument(..., help="File to review and fix"),
34
+ auto_apply: bool = typer.Option(False, "--apply", "-y", help="Auto-apply fixes without confirmation"),
35
+ check: str = typer.Option("all", help="Check type: bugs, security, performance, all"),
36
+ use_llm: bool = typer.Option(False, "--llm", help="Use LLM for deeper analysis"),
37
+ ):
38
+ """🔍 Complete code review → diagnose → fix workflow.
39
+
40
+ Runs a full code review, diagnoses issues, and optionally applies fixes.
41
+
42
+ Example:
43
+ cg review-and-fix src/auth.py
44
+ cg review-and-fix src/auth.py --apply
45
+ cg review-and-fix src/auth.py --llm --apply
46
+ """
47
+ from .bug_detector import BugDetector
48
+ from .security_scanner import SecurityScanner
49
+ from .performance_analyzer import PerformanceAnalyzer
50
+ from .llm import LocalLLM
51
+ from .validation_engine import ValidationEngine
52
+ from .models_v2 import CodeProposal
53
+
54
+ pm, project = _ensure_project()
55
+
56
+ file_path = Path(path)
57
+ if not file_path.exists():
58
+ console.print(f"[red]✗[/red] File not found: {path}")
59
+ raise typer.Exit(1)
60
+
61
+ project_dir = pm.project_dir(project)
62
+ store = GraphStore(project_dir)
63
+ llm = LocalLLM(
64
+ model=config.LLM_MODEL,
65
+ provider=config.LLM_PROVIDER,
66
+ api_key=config.LLM_API_KEY,
67
+ ) if use_llm else None
68
+
69
+ # ── Step 1: Code Review ──────────────────────────────────
70
+ console.print(f"\n[bold cyan]Step 1/3:[/bold cyan] Running code review on [bold]{path}[/bold]\n")
71
+
72
+ all_issues = []
73
+
74
+ if check in ("bugs", "all"):
75
+ detector = BugDetector(store, llm)
76
+ bug_issues = detector.analyze_file(str(file_path), use_llm=use_llm)
77
+ all_issues.extend(bug_issues)
78
+ console.print(f" [dim]Bug detection:[/dim] {len(bug_issues)} issue(s)")
79
+
80
+ if check in ("security", "all"):
81
+ scanner = SecurityScanner(store)
82
+ security_issues = scanner.scan_file(str(file_path))
83
+ all_issues.extend(security_issues)
84
+ console.print(f" [dim]Security scan:[/dim] {len(security_issues)} issue(s)")
85
+
86
+ if check in ("performance", "all"):
87
+ analyzer = PerformanceAnalyzer(store)
88
+ perf_issues = analyzer.analyze_file(str(file_path))
89
+ all_issues.extend(perf_issues)
90
+ console.print(f" [dim]Performance:[/dim] {len(perf_issues)} issue(s)")
91
+
92
+ if not all_issues:
93
+ console.print("\n[green]✓ No issues found! Code looks good.[/green]")
94
+ store.close()
95
+ return
96
+
97
+ # Show summary
98
+ by_severity = {"critical": 0, "high": 0, "medium": 0, "low": 0}
99
+ for issue in all_issues:
100
+ sev = issue.get("severity", "low")
101
+ by_severity[sev] = by_severity.get(sev, 0) + 1
102
+
103
+ console.print(f"\n Found [bold]{len(all_issues)}[/bold] total issues:")
104
+ for sev, count in by_severity.items():
105
+ if count:
106
+ icon = {"critical": "🚨", "high": "🔴", "medium": "⚠️", "low": "ℹ️"}.get(sev, "•")
107
+ console.print(f" {icon} {sev}: {count}")
108
+
109
+ # ── Step 2: Diagnose ─────────────────────────────────────
110
+ console.print(f"\n[bold cyan]Step 2/3:[/bold cyan] Diagnosing fixable issues\n")
111
+
112
+ validator = ValidationEngine()
113
+ errors = validator.diagnose_project(file_path.parent if file_path.is_file() else file_path)
114
+
115
+ if errors:
116
+ console.print(f" Found [bold]{len(errors)}[/bold] syntax error(s)")
117
+ else:
118
+ console.print(" [green]✓[/green] No syntax errors")
119
+
120
+ # ── Step 3: Fix ──────────────────────────────────────────
121
+ fixable = [e for e in errors if True] # all errors are potentially fixable
122
+ if not fixable:
123
+ console.print(f"\n[bold cyan]Step 3/3:[/bold cyan] No auto-fixable issues found")
124
+ console.print("\n[yellow]Review the issues above and fix manually.[/yellow]")
125
+
126
+ # Show issues for manual review
127
+ for issue in all_issues[:10]:
128
+ line = issue.get("line", "?")
129
+ msg = issue.get("message", issue.get("error", "Unknown"))
130
+ sev = issue.get("severity", "low")
131
+ console.print(f" [dim]L{line}[/dim] [{sev}] {msg}")
132
+ if "suggestion" in issue:
133
+ console.print(f" 💡 {issue['suggestion']}")
134
+
135
+ store.close()
136
+ return
137
+
138
+ console.print(f"\n[bold cyan]Step 3/3:[/bold cyan] Proposing fixes\n")
139
+
140
+ changes = []
141
+ for error in fixable:
142
+ file_p = Path(error["file"])
143
+ fix = validator.fix_common_errors(file_p)
144
+ if fix:
145
+ changes.append(fix)
146
+
147
+ if not changes:
148
+ console.print("[yellow]No automatic fixes available — manual fixes required.[/yellow]")
149
+ store.close()
150
+ return
151
+
152
+ diff_engine = DiffEngine()
153
+ proposal = CodeProposal(
154
+ id="review-and-fix",
155
+ description=f"Review & fix: {len(changes)} issue(s) in {path}",
156
+ changes=changes,
157
+ )
158
+ preview = diff_engine.preview_changes(proposal)
159
+ console.print(preview)
160
+
161
+ if auto_apply:
162
+ result = diff_engine.apply_changes(proposal, backup=True)
163
+ if result.success:
164
+ console.print(f"\n[green]✓ Fixed {len(result.files_changed)} file(s)[/green]")
165
+ if result.backup_id:
166
+ console.print(f"[dim]Backup: {result.backup_id} — rollback with: cg v2 rollback {result.backup_id}[/dim]")
167
+ else:
168
+ console.print(f"\n[red]✗ Failed to apply fixes: {result.error}[/red]")
169
+ else:
170
+ if typer.confirm("\nApply these fixes?", default=False):
171
+ result = diff_engine.apply_changes(proposal, backup=True)
172
+ if result.success:
173
+ console.print(f"\n[green]✓ Fixed {len(result.files_changed)} file(s)[/green]")
174
+ if result.backup_id:
175
+ console.print(f"[dim]Backup: {result.backup_id}[/dim]")
176
+ else:
177
+ console.print(f"\n[red]✗ Failed: {result.error}[/red]")
178
+ else:
179
+ console.print("[yellow]Fixes not applied.[/yellow]")
180
+
181
+ store.close()
182
+
183
+
184
+ @workflows_app.command("full-analysis")
185
+ def full_analysis(
186
+ symbol: str = typer.Argument(..., help="Symbol to analyze"),
187
+ hops: int = typer.Option(2, min=1, max=6, help="Dependency traversal depth."),
188
+ export: bool = typer.Option(False, "--export", "-e", help="Export graph to HTML"),
189
+ ):
190
+ """📊 Complete analysis: impact + graph + RAG context for a symbol.
191
+
192
+ Example:
193
+ cg full-analysis "UserService.login"
194
+ cg full-analysis "process_payment" --export
195
+ cg full-analysis "main" --hops 3
196
+ """
197
+ from .orchestrator import MCPOrchestrator
198
+ from .graph_export import export_html
199
+
200
+ pm, project = _ensure_project()
201
+ project_dir = pm.project_dir(project)
202
+ store = GraphStore(project_dir)
203
+ orchestrator = MCPOrchestrator(store)
204
+
205
+ console.print(f"\n[bold]Analyzing: [cyan]{symbol}[/cyan][/bold]\n")
206
+
207
+ # 1. Impact analysis
208
+ console.print("[cyan]1/3 Impact analysis...[/cyan]")
209
+ report = orchestrator.impact(symbol, hops=hops)
210
+
211
+ if "not found" in report.explanation.lower() and not report.impacted:
212
+ console.print(f"[red]✗[/red] Symbol '{symbol}' not found in current project.")
213
+ search_results = orchestrator.search(symbol, top_k=3)
214
+ if search_results:
215
+ console.print("\n[yellow]Did you mean:[/yellow]")
216
+ for r in search_results:
217
+ console.print(f" • {r.qualname} ({r.node_type})")
218
+ store.close()
219
+ raise typer.Exit(1)
220
+
221
+ console.print(f" Root: {report.root}")
222
+ console.print(f" Impacted: {len(report.impacted)} symbol(s)")
223
+ if report.impacted:
224
+ for imp in report.impacted[:10]:
225
+ console.print(f" • {imp}")
226
+ if len(report.impacted) > 10:
227
+ console.print(f" [dim]... and {len(report.impacted) - 10} more[/dim]")
228
+
229
+ # 2. Dependency graph
230
+ console.print("\n[cyan]2/3 Dependency graph...[/cyan]")
231
+ graph_text = orchestrator.graph(symbol, depth=hops)
232
+ console.print(graph_text)
233
+
234
+ # 3. RAG context
235
+ console.print("\n[cyan]3/3 RAG context...[/cyan]")
236
+ rag_text = orchestrator.rag_context(symbol, top_k=6)
237
+ # Count snippets (rough heuristic)
238
+ snippet_count = rag_text.count("──") if rag_text else 0
239
+ console.print(f" Retrieved {max(snippet_count, 1)} context snippet(s)")
240
+
241
+ # Summary
242
+ console.print(f"\n[bold green]━━━ Summary ━━━[/bold green]")
243
+ console.print(f" Symbol: {report.root}")
244
+ console.print(f" Impacted: {len(report.impacted)} symbol(s)")
245
+ console.print(f" Traversal: {hops} hop(s)")
246
+
247
+ console.print(f"\n[bold]Explanation:[/bold]")
248
+ console.print(report.explanation)
249
+
250
+ if export:
251
+ output_path = Path.cwd() / f"{symbol.replace('.', '_')}_graph.html"
252
+ export_html(store, output_path, focus=symbol)
253
+ console.print(f"\n[green]✓ Graph exported to {output_path}[/green]")
254
+
255
+ store.close()
@@ -135,7 +135,21 @@ class CodeGenAgent:
135
135
  Returns:
136
136
  Result of applying changes
137
137
  """
138
- return self.diff_engine.apply_changes(proposal, backup=backup)
138
+ result = self.diff_engine.apply_changes(proposal, backup=backup)
139
+
140
+ # Incrementally reindex changed files so the code graph stays current
141
+ if result.success and self.project_context is not None:
142
+ for change in proposal.changes:
143
+ try:
144
+ rel_path = change.file_path
145
+ if change.change_type == "delete":
146
+ self.project_context.remove_from_index(rel_path)
147
+ else:
148
+ self.project_context._incremental_reindex(rel_path)
149
+ except Exception:
150
+ pass # never break apply on index failure
151
+
152
+ return result
139
153
 
140
154
  def _gather_context(
141
155
  self,
codegraph_cli/config.py CHANGED
@@ -11,6 +11,14 @@ STATE_FILE = BASE_DIR / "state.json"
11
11
  DEFAULT_EMBEDDING_DIM = 256
12
12
  SUPPORTED_EXTENSIONS = {".py"}
13
13
 
14
+ # Smart defaults — work without any configuration
15
+ DEFAULT_CONFIG = {
16
+ "llm_provider": "ollama",
17
+ "llm_model": "qwen2.5-coder:7b",
18
+ "embedding_model": "hash",
19
+ "auto_setup": True,
20
+ }
21
+
14
22
  # Load configuration from TOML file (if available)
15
23
  try:
16
24
  from .config_manager import load_config, load_embedding_config
@@ -20,14 +28,19 @@ except ImportError:
20
28
  _toml_config = {}
21
29
  _emb_config = {}
22
30
 
23
- # LLM Provider Configuration — loaded from ~/.codegraph/config.toml (set via `cg setup` or `cg set-llm`)
24
- LLM_PROVIDER = _toml_config.get("provider", "ollama")
31
+ # LLM Provider Configuration — loaded from ~/.codegraph/config.toml (set via `cg config setup` or `cg config set-llm`)
32
+ LLM_PROVIDER = _toml_config.get("provider", DEFAULT_CONFIG["llm_provider"])
25
33
  LLM_API_KEY = _toml_config.get("api_key", "")
26
- LLM_MODEL = _toml_config.get("model", "qwen2.5-coder:7b")
34
+ LLM_MODEL = _toml_config.get("model", DEFAULT_CONFIG["llm_model"])
27
35
  LLM_ENDPOINT = _toml_config.get("endpoint", "http://127.0.0.1:11434/api/generate")
28
36
 
29
- # Embedding model — set via `cg set-embedding` (default: "hash" = no download)
30
- EMBEDDING_MODEL = _emb_config.get("model", "hash")
37
+ # Embedding model — set via `cg config set-embedding` (default: "hash" = no download)
38
+ EMBEDDING_MODEL = _emb_config.get("model", DEFAULT_CONFIG["embedding_model"])
39
+
40
+
41
+ def config_file_exists() -> bool:
42
+ """Check whether a user config file has been created."""
43
+ return (BASE_DIR / "config.toml").exists()
31
44
 
32
45
 
33
46
  def ensure_base_dirs() -> None:
@@ -5,6 +5,8 @@ Includes:
5
5
  symbols) designed to fit inside an LLM context window for agentic planning
6
6
  *before* deep retrieval.
7
7
  - **ConversationMemory**: sliding-window compression for chat history.
8
+ - **SymbolMemory**: tracks recently discussed symbols, files, and proposals
9
+ so the chat agent can avoid redundant RAG queries.
8
10
  - Intent detection and query extraction utilities.
9
11
  """
10
12
 
@@ -250,6 +252,63 @@ class ConversationMemory:
250
252
  return " | ".join(summary_parts[-5:])
251
253
 
252
254
 
255
+ class SymbolMemory:
256
+ """Tracks recently discussed symbols, files, and active proposals.
257
+
258
+ The chat agent consults this memory *before* hitting RAG so that
259
+ consecutive questions about the same symbol don't trigger redundant
260
+ vector searches.
261
+ """
262
+
263
+ def __init__(self, max_symbols: int = 20, max_files: int = 10) -> None:
264
+ self.max_symbols = max_symbols
265
+ self.max_files = max_files
266
+
267
+ # Recently discussed symbols (qualname → last SearchResult)
268
+ self.recent_symbols: Dict[str, Any] = {}
269
+ # Recently touched files
270
+ self.recent_files: List[str] = []
271
+ # Active proposal (if any)
272
+ self.active_proposal: Optional[CodeProposal] = None
273
+
274
+ def record_symbols(self, results: List[Any]) -> None:
275
+ """Record search results as recently discussed symbols."""
276
+ for r in results:
277
+ self.recent_symbols[r.qualname] = r
278
+ # Trim to limit
279
+ while len(self.recent_symbols) > self.max_symbols:
280
+ # Remove oldest (first inserted)
281
+ oldest_key = next(iter(self.recent_symbols))
282
+ del self.recent_symbols[oldest_key]
283
+
284
+ def record_file(self, file_path: str) -> None:
285
+ """Track a file that was recently discussed or modified."""
286
+ if file_path in self.recent_files:
287
+ self.recent_files.remove(file_path)
288
+ self.recent_files.insert(0, file_path)
289
+ self.recent_files = self.recent_files[: self.max_files]
290
+
291
+ def get_context_summary(self) -> str:
292
+ """Return a compact summary of tracked state for LLM context."""
293
+ parts: List[str] = []
294
+ if self.recent_symbols:
295
+ names = list(self.recent_symbols.keys())[-8:]
296
+ parts.append(f"Recent symbols: {', '.join(names)}")
297
+ if self.recent_files:
298
+ parts.append(f"Recent files: {', '.join(self.recent_files[:5])}")
299
+ if self.active_proposal:
300
+ parts.append(f"Active proposal: {self.active_proposal.description}")
301
+ return "\n".join(parts)
302
+
303
+ def find_cached_symbol(self, query: str) -> Optional[Any]:
304
+ """Check if *query* matches a recently discussed symbol."""
305
+ query_lower = query.lower()
306
+ for qualname, result in self.recent_symbols.items():
307
+ if query_lower in qualname.lower() or qualname.lower() in query_lower:
308
+ return result
309
+ return None
310
+
311
+
253
312
  def detect_intent(message: str) -> str:
254
313
  """Detect user intent from message.
255
314
 
@@ -343,11 +402,24 @@ def extract_queries_from_message(message: str, intent: str) -> List[str]:
343
402
  queries.append(f"{target} service")
344
403
 
345
404
  elif intent in ["impact", "explain"]:
346
- # Extract symbol names
347
- # Look for function/class names (CamelCase or snake_case)
348
- symbols = re.findall(r'\b[A-Z][a-zA-Z0-9_]*\b|\b[a-z_][a-z0-9_]*\b', message)
349
- if symbols:
350
- queries.append(symbols[0]) # Use first symbol
405
+ # Extract symbol names — prefer CamelCase and snake_case
406
+ # identifiers that look like real code symbols, not common
407
+ # English words.
408
+ camel_case = re.findall(r'\b[A-Z][a-zA-Z0-9_]+\b', message)
409
+ snake_case = re.findall(r'\b[a-z_][a-z0-9_]*(?:_[a-z0-9]+)+\b', message)
410
+ code_symbols = camel_case + snake_case
411
+ if code_symbols:
412
+ queries.append(code_symbols[0])
413
+ else:
414
+ # Fallback: extract all identifier-like tokens, skip
415
+ # very short common English words.
416
+ _SKIP = {"how", "does", "what", "why", "the", "is", "in", "a", "an", "to", "for", "it", "of", "on"}
417
+ symbols = [
418
+ s for s in re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', message)
419
+ if s.lower() not in _SKIP and len(s) > 2
420
+ ]
421
+ if symbols:
422
+ queries.append(symbols[0])
351
423
 
352
424
  # Fallback: use entire message
353
425
  if not queries:
@@ -374,7 +446,8 @@ def assemble_context_for_llm(
374
446
  session: ChatSession,
375
447
  rag_retriever: RAGRetriever,
376
448
  system_prompt: str,
377
- max_tokens: int = 8000
449
+ max_tokens: int = 8000,
450
+ symbol_memory: Optional[SymbolMemory] = None,
378
451
  ) -> List[Dict[str, str]]:
379
452
  """Assemble optimized context for LLM within token budget.
380
453
 
@@ -384,6 +457,7 @@ def assemble_context_for_llm(
384
457
  rag_retriever: RAG retriever for code search
385
458
  system_prompt: System prompt
386
459
  max_tokens: Maximum total tokens
460
+ symbol_memory: Optional SymbolMemory for tracking context
387
461
 
388
462
  Returns:
389
463
  List of message dicts for LLM
@@ -398,7 +472,12 @@ def assemble_context_for_llm(
398
472
  # 2. Detect intent
399
473
  intent = detect_intent(user_message)
400
474
 
401
- # 3. Extract queries and retrieve code
475
+ # 3. Check symbol memory first (avoid redundant RAG)
476
+ memory_hit = None
477
+ if symbol_memory is not None:
478
+ memory_hit = symbol_memory.find_cached_symbol(user_message)
479
+
480
+ # 4. Extract queries and retrieve code
402
481
  queries = extract_queries_from_message(user_message, intent)
403
482
  code_snippets = []
404
483
 
@@ -419,7 +498,25 @@ def assemble_context_for_llm(
419
498
  # Limit to top 10
420
499
  unique_snippets = unique_snippets[:10]
421
500
 
422
- # 4. Add RAG context (high priority)
501
+ # Record in symbol memory
502
+ if symbol_memory is not None and unique_snippets:
503
+ symbol_memory.record_symbols(unique_snippets)
504
+ for s in unique_snippets:
505
+ symbol_memory.record_file(s.file_path)
506
+
507
+ # 5. Add symbol memory summary (compact, low cost)
508
+ if symbol_memory is not None:
509
+ mem_summary = symbol_memory.get_context_summary()
510
+ if mem_summary:
511
+ mem_tokens = count_tokens(mem_summary)
512
+ if token_count + mem_tokens < max_tokens - 3000:
513
+ context.append({
514
+ "role": "system",
515
+ "content": f"Session context:\n{mem_summary}",
516
+ })
517
+ token_count += mem_tokens
518
+
519
+ # 6. Add RAG context (high priority)
423
520
  if unique_snippets:
424
521
  rag_context = format_code_snippets(unique_snippets)
425
522
  rag_tokens = count_tokens(rag_context)
@@ -431,7 +528,7 @@ def assemble_context_for_llm(
431
528
  })
432
529
  token_count += rag_tokens
433
530
 
434
- # 5. Add pending proposals if exist
531
+ # 7. Add pending proposals if exist
435
532
  if session.pending_proposals:
436
533
  proposal_text = format_proposals(session.pending_proposals)
437
534
  proposal_tokens = count_tokens(proposal_text)
@@ -443,7 +540,7 @@ def assemble_context_for_llm(
443
540
  })
444
541
  token_count += proposal_tokens
445
542
 
446
- # 6. Add conversation history (compressed)
543
+ # 8. Add conversation history (compressed)
447
544
  conv_memory = ConversationMemory(max_recent=3)
448
545
  conv_context = conv_memory.get_context_for_llm(
449
546
  session,
@@ -451,14 +548,16 @@ def assemble_context_for_llm(
451
548
  )
452
549
  context.extend(conv_context)
453
550
 
454
- # 7. Add current user message
551
+ # 9. Add current user message
455
552
  context.append({"role": "user", "content": user_message})
456
553
 
457
554
  return context
458
555
 
459
556
 
460
557
  def format_code_snippets(snippets: List) -> str:
461
- """Format code snippets for LLM context.
558
+ """Format code snippets for LLM context with compression.
559
+
560
+ Strips import lines, trims long code, and adds structured metadata.
462
561
 
463
562
  Args:
464
563
  snippets: List of SearchResult objects
@@ -466,14 +565,17 @@ def format_code_snippets(snippets: List) -> str:
466
565
  Returns:
467
566
  Formatted string
468
567
  """
568
+ from .rag import _compress_snippet
569
+
469
570
  blocks = []
470
571
 
471
572
  for snippet in snippets:
573
+ compressed = _compress_snippet(snippet.snippet, max_chars=800)
472
574
  blocks.append(
473
575
  f"[{snippet.node_type}] {snippet.qualname}\n"
474
- f"Location: {snippet.file_path}:{snippet.start_line}\n"
475
- f"Relevance: {snippet.score:.2f}\n"
476
- f"```python\n{snippet.snippet[:800]}\n```"
576
+ f"file: {snippet.file_path}:{snippet.start_line}\n"
577
+ f"score: {snippet.score:.2f}\n"
578
+ f"```python\n{compressed}\n```"
477
579
  )
478
580
 
479
581
  return "\n\n".join(blocks)
@@ -4,7 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING, List
6
6
 
7
- from crewai import Agent
7
+ try:
8
+ from crewai import Agent
9
+ CREWAI_AVAILABLE = True
10
+ except ImportError:
11
+ Agent = None # type: ignore
12
+ CREWAI_AVAILABLE = False
8
13
 
9
14
  if TYPE_CHECKING:
10
15
  from .crew_tools import create_tools
@@ -32,6 +37,7 @@ def create_file_ops_agent(tools: list, llm, project_context: str = "") -> Agent:
32
37
  verbose=False,
33
38
  allow_delegation=False,
34
39
  max_iter=15,
40
+ max_tokens=4096,
35
41
  )
36
42
 
37
43
 
@@ -54,13 +60,17 @@ def create_code_gen_agent(tools: list, llm, project_context: str = "") -> Agent:
54
60
  "4. Include proper error handling, type hints, and docstrings\n"
55
61
  "5. Use search_code and grep_in_project to understand how code is used before changing it\n"
56
62
  "6. When asked to improve/refactor, explain what you changed and why\n"
57
- f"7. Generate complete, runnable code — never leave TODO placeholders{ctx}"
63
+ "7. Generate complete, runnable code — never leave TODO placeholders\n"
64
+ "8. When applying previously suggested improvements, check the PREVIOUS CONVERSATION "
65
+ "section in the task description for the exact changes that were recommended, then "
66
+ f"implement each one using the appropriate tools{ctx}"
58
67
  ),
59
68
  tools=tools,
60
69
  llm=llm,
61
70
  verbose=False,
62
71
  allow_delegation=False,
63
72
  max_iter=20,
73
+ max_tokens=4096,
64
74
  )
65
75
 
66
76
 
@@ -89,10 +99,11 @@ def create_code_analysis_agent(tools: list, llm, project_context: str = "") -> A
89
99
  verbose=False,
90
100
  allow_delegation=False,
91
101
  max_iter=15,
102
+ max_tokens=4096,
92
103
  )
93
104
 
94
105
 
95
- def create_coordinator_agent(llm, project_context: str = "") -> Agent:
106
+ def create_coordinator_agent(llm, project_context: str = "", tools: list = None) -> Agent:
96
107
  """Coordinator agent — routes tasks to the right specialist."""
97
108
  ctx = f" Current Project: {project_context}." if project_context else ""
98
109
  return Agent(
@@ -100,7 +111,8 @@ def create_coordinator_agent(llm, project_context: str = "") -> Agent:
100
111
  goal=(
101
112
  "Understand user requests and coordinate specialist agents. Route file operations "
102
113
  "to File System Engineer, code changes to Senior Software Developer, and analysis "
103
- "to Code Intelligence Analyst."
114
+ "to Code Intelligence Analyst. When users reference previous conversation, use the "
115
+ "provided history to fulfil follow-up requests."
104
116
  ),
105
117
  backstory=(
106
118
  f"You are a project coordinator managing a team of AI specialists.{ctx}\n\n"
@@ -109,15 +121,27 @@ def create_coordinator_agent(llm, project_context: str = "") -> Agent:
109
121
  "• Senior Software Developer — generates code, implements features, refactors\n"
110
122
  "• Code Intelligence Analyst — searches code, analyzes dependencies, explains logic\n\n"
111
123
  "RULES:\n"
112
- "1. ALWAYS delegate to the right specialist never try to do tasks yourself\n"
113
- "2. For 'what is in this project' / 'show files' → delegate to File System Engineer\n"
124
+ "1. ALWAYS use tools directly to gather information BEFORE responding. "
125
+ "Call read_file, file_tree, search_code, get_project_summary, etc. "
126
+ "Never say 'I need to examine files' without actually doing it in the same step.\n"
127
+ "2. For complex file operations or code generation, delegate to the appropriate specialist\n"
114
128
  "3. For 'write code' / 'add feature' / 'fix bug' / 'refactor' → delegate to Senior Software Developer\n"
115
129
  "4. For 'find' / 'search' / 'explain' / 'how does X work' → delegate to Code Intelligence Analyst\n"
116
130
  "5. For complex tasks, break them into steps and delegate each step\n"
117
- "6. Always return concrete answers based on actual project data from tools"
131
+ "6. Always return concrete answers based on actual project data from tools\n"
132
+ "7. CRITICAL — CONVERSATION CONTINUITY: When the user says things like "
133
+ "'apply those', 'implement that', 'do what you said', 'make those changes', or "
134
+ "references previous suggestions, look at the PREVIOUS CONVERSATION section in the "
135
+ "task description. It contains the full history of what was discussed. Extract the "
136
+ "specific suggestions/improvements from your earlier response and delegate to the "
137
+ "Senior Software Developer to ACTUALLY implement them using write_file/patch_file tools. "
138
+ "Never say you don't remember or can't access previous context — it's right there "
139
+ "in the task description."
118
140
  ),
141
+ tools=tools,
119
142
  llm=llm,
120
143
  verbose=False,
121
144
  allow_delegation=True,
122
- max_iter=10,
145
+ max_iter=25,
146
+ max_tokens=4096,
123
147
  )