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.
- codegraph_cli/__init__.py +1 -1
- codegraph_cli/agents.py +59 -3
- codegraph_cli/chat_agent.py +58 -11
- codegraph_cli/cli.py +569 -54
- codegraph_cli/cli_chat.py +204 -94
- codegraph_cli/cli_diagnose.py +13 -2
- codegraph_cli/cli_docs.py +207 -0
- codegraph_cli/cli_explore.py +1053 -0
- codegraph_cli/cli_export.py +941 -0
- codegraph_cli/cli_groups.py +33 -0
- codegraph_cli/cli_health.py +316 -0
- codegraph_cli/cli_history.py +213 -0
- codegraph_cli/cli_onboard.py +380 -0
- codegraph_cli/cli_quickstart.py +256 -0
- codegraph_cli/cli_refactor.py +17 -3
- codegraph_cli/cli_setup.py +12 -12
- codegraph_cli/cli_suggestions.py +90 -0
- codegraph_cli/cli_test.py +17 -3
- codegraph_cli/cli_tui.py +210 -0
- codegraph_cli/cli_v2.py +24 -4
- codegraph_cli/cli_watch.py +158 -0
- codegraph_cli/cli_workflows.py +255 -0
- codegraph_cli/codegen_agent.py +15 -1
- codegraph_cli/config.py +18 -5
- codegraph_cli/context_manager.py +117 -15
- codegraph_cli/crew_agents.py +32 -8
- codegraph_cli/crew_chat.py +146 -13
- codegraph_cli/crew_tools.py +30 -2
- codegraph_cli/embeddings.py +95 -5
- codegraph_cli/llm.py +42 -55
- codegraph_cli/project_context.py +64 -1
- codegraph_cli/rag.py +282 -19
- codegraph_cli/storage.py +310 -14
- codegraph_cli/vector_store.py +110 -8
- {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/METADATA +75 -21
- codegraph_cli-2.1.2.dist-info/RECORD +55 -0
- codegraph_cli-2.1.2.dist-info/entry_points.txt +2 -0
- codegraph_cli-2.1.0.dist-info/RECORD +0 -43
- codegraph_cli-2.1.0.dist-info/entry_points.txt +0 -2
- {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/WHEEL +0 -0
- {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {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()
|
codegraph_cli/codegen_agent.py
CHANGED
|
@@ -135,7 +135,21 @@ class CodeGenAgent:
|
|
|
135
135
|
Returns:
|
|
136
136
|
Result of applying changes
|
|
137
137
|
"""
|
|
138
|
-
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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:
|
codegraph_cli/context_manager.py
CHANGED
|
@@ -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
|
-
#
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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"
|
|
475
|
-
f"
|
|
476
|
-
f"```python\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)
|
codegraph_cli/crew_agents.py
CHANGED
|
@@ -4,7 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING, List
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
113
|
-
"
|
|
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=
|
|
145
|
+
max_iter=25,
|
|
146
|
+
max_tokens=4096,
|
|
123
147
|
)
|