codegraph-cli 2.0.0__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 +4 -0
- codegraph_cli/agents.py +191 -0
- codegraph_cli/bug_detector.py +386 -0
- codegraph_cli/chat_agent.py +352 -0
- codegraph_cli/chat_session.py +220 -0
- codegraph_cli/cli.py +330 -0
- codegraph_cli/cli_chat.py +367 -0
- codegraph_cli/cli_diagnose.py +133 -0
- codegraph_cli/cli_refactor.py +230 -0
- codegraph_cli/cli_setup.py +470 -0
- codegraph_cli/cli_test.py +177 -0
- codegraph_cli/cli_v2.py +267 -0
- codegraph_cli/codegen_agent.py +265 -0
- codegraph_cli/config.py +31 -0
- codegraph_cli/config_manager.py +341 -0
- codegraph_cli/context_manager.py +500 -0
- codegraph_cli/crew_agents.py +123 -0
- codegraph_cli/crew_chat.py +159 -0
- codegraph_cli/crew_tools.py +497 -0
- codegraph_cli/diff_engine.py +265 -0
- codegraph_cli/embeddings.py +241 -0
- codegraph_cli/graph_export.py +144 -0
- codegraph_cli/llm.py +642 -0
- codegraph_cli/models.py +47 -0
- codegraph_cli/models_v2.py +185 -0
- codegraph_cli/orchestrator.py +49 -0
- codegraph_cli/parser.py +800 -0
- codegraph_cli/performance_analyzer.py +223 -0
- codegraph_cli/project_context.py +230 -0
- codegraph_cli/rag.py +200 -0
- codegraph_cli/refactor_agent.py +452 -0
- codegraph_cli/security_scanner.py +366 -0
- codegraph_cli/storage.py +390 -0
- codegraph_cli/templates/graph_interactive.html +257 -0
- codegraph_cli/testgen_agent.py +316 -0
- codegraph_cli/validation_engine.py +285 -0
- codegraph_cli/vector_store.py +293 -0
- codegraph_cli-2.0.0.dist-info/METADATA +318 -0
- codegraph_cli-2.0.0.dist-info/RECORD +43 -0
- codegraph_cli-2.0.0.dist-info/WHEEL +5 -0
- codegraph_cli-2.0.0.dist-info/entry_points.txt +2 -0
- codegraph_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_cli-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""Smart context management for chat mode using RAG.
|
|
2
|
+
|
|
3
|
+
Includes:
|
|
4
|
+
- **RepoMap**: lightweight tree representation of the codebase (filenames +
|
|
5
|
+
symbols) designed to fit inside an LLM context window for agentic planning
|
|
6
|
+
*before* deep retrieval.
|
|
7
|
+
- **ConversationMemory**: sliding-window compression for chat history.
|
|
8
|
+
- Intent detection and query extraction utilities.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
17
|
+
|
|
18
|
+
from .models_v2 import ChatMessage, ChatSession, CodeProposal
|
|
19
|
+
from .rag import RAGRetriever
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Directories to skip when building the repo map
|
|
24
|
+
_REPO_MAP_SKIP: Set[str] = {
|
|
25
|
+
".venv", "venv", "__pycache__", "node_modules", ".git",
|
|
26
|
+
"site-packages", ".tox", ".pytest_cache", "build", "dist",
|
|
27
|
+
".mypy_cache", ".ruff_cache", "htmlcov", ".eggs",
|
|
28
|
+
"egg-info", ".codegraph", "lancedb", ".chroma",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# File extensions treated as "source code" in the repo map
|
|
32
|
+
_SOURCE_EXTENSIONS: Set[str] = {
|
|
33
|
+
".py", ".js", ".ts", ".tsx", ".jsx",
|
|
34
|
+
".go", ".rs", ".java", ".rb",
|
|
35
|
+
".cpp", ".c", ".cs", ".h", ".hpp",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ===================================================================
|
|
40
|
+
# RepoMap – Agentic Context Feature
|
|
41
|
+
# ===================================================================
|
|
42
|
+
|
|
43
|
+
class RepoMap:
|
|
44
|
+
"""Generate a lightweight tree representation of a codebase.
|
|
45
|
+
|
|
46
|
+
The map lists every file together with its top-level symbols (classes,
|
|
47
|
+
functions) and is compact enough to inject into an LLM context window.
|
|
48
|
+
This gives the model a *birds-eye view* of the repo **before** it
|
|
49
|
+
performs targeted retrieval.
|
|
50
|
+
|
|
51
|
+
Example output::
|
|
52
|
+
|
|
53
|
+
codegraph_cli/parser.py
|
|
54
|
+
class: TreeSitterParser
|
|
55
|
+
class: ASTFallbackParser
|
|
56
|
+
class: PythonGraphParser
|
|
57
|
+
function: _resolve_call_edges
|
|
58
|
+
codegraph_cli/embeddings.py
|
|
59
|
+
class: NeuralEmbedder
|
|
60
|
+
class: HashEmbeddingModel
|
|
61
|
+
function: get_embedder
|
|
62
|
+
function: cosine_similarity
|
|
63
|
+
|
|
64
|
+
Usage::
|
|
65
|
+
|
|
66
|
+
repo_map = RepoMap(project_root, parser=my_parser)
|
|
67
|
+
context = repo_map.generate(max_tokens=4000)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
project_root: Path,
|
|
73
|
+
parser: Optional[Any] = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Args:
|
|
77
|
+
project_root: Root directory of the project.
|
|
78
|
+
parser: Optional :class:`~codegraph_cli.parser.Parser` instance.
|
|
79
|
+
When provided, the map includes symbols per file.
|
|
80
|
+
"""
|
|
81
|
+
self.project_root = project_root
|
|
82
|
+
self.parser = parser
|
|
83
|
+
|
|
84
|
+
def generate(self, max_tokens: int = 4000) -> str:
|
|
85
|
+
"""Build the repo map string, truncated to *max_tokens*.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
max_tokens: Approximate token budget (1 token ~ 4 chars).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
A compact, indented text representation of the repo structure.
|
|
92
|
+
"""
|
|
93
|
+
tree_lines: List[str] = []
|
|
94
|
+
|
|
95
|
+
for file_path in sorted(self.project_root.rglob("*")):
|
|
96
|
+
if any(part in _REPO_MAP_SKIP for part in file_path.parts):
|
|
97
|
+
continue
|
|
98
|
+
if file_path.is_dir():
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
rel = file_path.relative_to(self.project_root)
|
|
102
|
+
|
|
103
|
+
# For source files, attempt to list symbols
|
|
104
|
+
if file_path.suffix in _SOURCE_EXTENSIONS and self.parser is not None:
|
|
105
|
+
try:
|
|
106
|
+
nodes, _ = self.parser.parse_file(file_path)
|
|
107
|
+
tree_lines.append(str(rel))
|
|
108
|
+
for node in nodes:
|
|
109
|
+
if node.node_type == "module":
|
|
110
|
+
continue
|
|
111
|
+
# Indent by nesting depth
|
|
112
|
+
depth = node.qualname.count(".") - str(rel).replace("/", ".").removesuffix(".py").count(".")
|
|
113
|
+
indent = " " * max(depth, 1)
|
|
114
|
+
tree_lines.append(f"{indent}{node.node_type}: {node.name}")
|
|
115
|
+
except Exception:
|
|
116
|
+
tree_lines.append(str(rel))
|
|
117
|
+
else:
|
|
118
|
+
tree_lines.append(str(rel))
|
|
119
|
+
|
|
120
|
+
# Truncate to fit token budget
|
|
121
|
+
result = "\n".join(tree_lines)
|
|
122
|
+
char_budget = max_tokens * 4
|
|
123
|
+
if len(result) > char_budget:
|
|
124
|
+
result = result[:char_budget]
|
|
125
|
+
# Clean cut at last newline
|
|
126
|
+
last_nl = result.rfind("\n")
|
|
127
|
+
if last_nl > 0:
|
|
128
|
+
result = result[:last_nl]
|
|
129
|
+
result += "\n... (truncated)"
|
|
130
|
+
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
def generate_for_files(self, file_paths: List[Path]) -> str:
|
|
134
|
+
"""Build a focused repo map for a subset of files.
|
|
135
|
+
|
|
136
|
+
Useful when the agent already knows which files are relevant.
|
|
137
|
+
"""
|
|
138
|
+
tree_lines: List[str] = []
|
|
139
|
+
for file_path in sorted(file_paths):
|
|
140
|
+
if not file_path.exists():
|
|
141
|
+
continue
|
|
142
|
+
try:
|
|
143
|
+
rel = file_path.relative_to(self.project_root)
|
|
144
|
+
except ValueError:
|
|
145
|
+
rel = file_path
|
|
146
|
+
if file_path.suffix in _SOURCE_EXTENSIONS and self.parser is not None:
|
|
147
|
+
try:
|
|
148
|
+
nodes, _ = self.parser.parse_file(file_path)
|
|
149
|
+
tree_lines.append(str(rel))
|
|
150
|
+
for node in nodes:
|
|
151
|
+
if node.node_type == "module":
|
|
152
|
+
continue
|
|
153
|
+
depth = node.qualname.count(".") - str(rel).replace("/", ".").removesuffix(".py").count(".")
|
|
154
|
+
indent = " " * max(depth, 1)
|
|
155
|
+
tree_lines.append(f"{indent}{node.node_type}: {node.name}")
|
|
156
|
+
except Exception:
|
|
157
|
+
tree_lines.append(str(rel))
|
|
158
|
+
else:
|
|
159
|
+
tree_lines.append(str(rel))
|
|
160
|
+
return "\n".join(tree_lines)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ConversationMemory:
|
|
164
|
+
"""Manages conversation history with compression for token efficiency."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, max_recent: int = 3):
|
|
167
|
+
"""Initialize conversation memory.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
max_recent: Number of recent messages to keep verbatim
|
|
171
|
+
"""
|
|
172
|
+
self.max_recent = max_recent
|
|
173
|
+
self.summary = ""
|
|
174
|
+
|
|
175
|
+
def get_context_for_llm(
|
|
176
|
+
self,
|
|
177
|
+
session: ChatSession,
|
|
178
|
+
token_budget: int = 1500
|
|
179
|
+
) -> List[Dict[str, str]]:
|
|
180
|
+
"""Get optimized conversation context for LLM.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
session: Chat session with messages
|
|
184
|
+
token_budget: Maximum tokens to use for conversation history
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of message dicts for LLM
|
|
188
|
+
"""
|
|
189
|
+
messages = session.messages
|
|
190
|
+
|
|
191
|
+
if len(messages) <= self.max_recent:
|
|
192
|
+
# All messages fit, return as-is
|
|
193
|
+
return [
|
|
194
|
+
{"role": msg.role, "content": msg.content}
|
|
195
|
+
for msg in messages
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
# Split into old and recent
|
|
199
|
+
old_messages = messages[:-self.max_recent]
|
|
200
|
+
recent_messages = messages[-self.max_recent:]
|
|
201
|
+
|
|
202
|
+
# Summarize old messages if not already done
|
|
203
|
+
if not self.summary and old_messages:
|
|
204
|
+
self.summary = self._summarize_messages(old_messages)
|
|
205
|
+
|
|
206
|
+
# Build context
|
|
207
|
+
context = []
|
|
208
|
+
|
|
209
|
+
# Add summary as system message
|
|
210
|
+
if self.summary:
|
|
211
|
+
context.append({
|
|
212
|
+
"role": "system",
|
|
213
|
+
"content": f"Previous conversation summary: {self.summary}"
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
# Add recent messages verbatim
|
|
217
|
+
context.extend([
|
|
218
|
+
{"role": msg.role, "content": msg.content}
|
|
219
|
+
for msg in recent_messages
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
return context
|
|
223
|
+
|
|
224
|
+
def _summarize_messages(self, messages: List[ChatMessage]) -> str:
|
|
225
|
+
"""Summarize old messages to save tokens.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
messages: Messages to summarize
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Summary string
|
|
232
|
+
"""
|
|
233
|
+
summary_parts = []
|
|
234
|
+
|
|
235
|
+
for msg in messages:
|
|
236
|
+
if msg.role == "user":
|
|
237
|
+
# Extract intent from user messages
|
|
238
|
+
content_preview = msg.content[:100].replace("\n", " ")
|
|
239
|
+
summary_parts.append(f"User: {content_preview}")
|
|
240
|
+
elif msg.role == "assistant":
|
|
241
|
+
# Extract actions from assistant messages
|
|
242
|
+
if "applied changes" in msg.content.lower():
|
|
243
|
+
summary_parts.append("Applied code changes")
|
|
244
|
+
elif "proposal" in msg.content.lower():
|
|
245
|
+
summary_parts.append("Created code proposal")
|
|
246
|
+
elif "refactor" in msg.content.lower():
|
|
247
|
+
summary_parts.append("Discussed refactoring")
|
|
248
|
+
|
|
249
|
+
# Keep last 5 actions
|
|
250
|
+
return " | ".join(summary_parts[-5:])
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def detect_intent(message: str) -> str:
|
|
254
|
+
"""Detect user intent from message.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
message: User message
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Intent string: list, search, generate, refactor, impact, explain, chat
|
|
261
|
+
"""
|
|
262
|
+
message_lower = message.lower()
|
|
263
|
+
|
|
264
|
+
# Read file intent (show/read specific file content)
|
|
265
|
+
if any(kw in message_lower for kw in [
|
|
266
|
+
"show me", "read", "what's in", "whats in", "what is in", "contents of",
|
|
267
|
+
"display", "view", "open", "cat"
|
|
268
|
+
]) and any(ext in message_lower for ext in [".py", ".txt", ".md", ".json", ".yaml", ".toml"]):
|
|
269
|
+
return "read"
|
|
270
|
+
|
|
271
|
+
# List intent (list files, show files, what files in project)
|
|
272
|
+
if any(kw in message_lower for kw in [
|
|
273
|
+
"list", "show files", "what files", "all files", "files in",
|
|
274
|
+
"what do we have", "what's here", "whats here", "list the things"
|
|
275
|
+
]):
|
|
276
|
+
return "list"
|
|
277
|
+
|
|
278
|
+
# Search intent
|
|
279
|
+
if any(kw in message_lower for kw in ["find", "search", "where is", "show me", "locate"]):
|
|
280
|
+
return "search"
|
|
281
|
+
|
|
282
|
+
# Generate intent
|
|
283
|
+
if any(kw in message_lower for kw in ["add", "create", "generate", "implement", "build", "make"]):
|
|
284
|
+
return "generate"
|
|
285
|
+
|
|
286
|
+
# Refactor intent
|
|
287
|
+
if any(kw in message_lower for kw in ["refactor", "extract", "rename", "move", "reorganize"]):
|
|
288
|
+
return "refactor"
|
|
289
|
+
|
|
290
|
+
# Impact intent
|
|
291
|
+
if any(kw in message_lower for kw in ["impact", "what breaks", "what depends", "who uses"]):
|
|
292
|
+
return "impact"
|
|
293
|
+
|
|
294
|
+
# Explain intent
|
|
295
|
+
if any(kw in message_lower for kw in ["explain", "what does", "how does", "why", "describe"]):
|
|
296
|
+
return "explain"
|
|
297
|
+
|
|
298
|
+
# Default to chat
|
|
299
|
+
return "chat"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def extract_queries_from_message(message: str, intent: str) -> List[str]:
|
|
303
|
+
"""Extract search queries from user message based on intent.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
message: User message
|
|
307
|
+
intent: Detected intent
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
List of search queries
|
|
311
|
+
"""
|
|
312
|
+
queries = []
|
|
313
|
+
|
|
314
|
+
if intent == "search":
|
|
315
|
+
# Direct search - use message as-is
|
|
316
|
+
queries.append(message)
|
|
317
|
+
|
|
318
|
+
elif intent == "generate":
|
|
319
|
+
# Extract domain concepts
|
|
320
|
+
# E.g., "Add password reset endpoint" -> ["password reset", "authentication", "email"]
|
|
321
|
+
|
|
322
|
+
# Extract main concept (first few words after action verb)
|
|
323
|
+
match = re.search(r'(?:add|create|implement|build|make)\s+(.+?)(?:\s+endpoint|\s+function|\s+class|$)', message, re.IGNORECASE)
|
|
324
|
+
if match:
|
|
325
|
+
main_concept = match.group(1).strip()
|
|
326
|
+
queries.append(main_concept)
|
|
327
|
+
|
|
328
|
+
# Add related concepts
|
|
329
|
+
if "password" in message.lower():
|
|
330
|
+
queries.extend(["authentication", "email sending", "token generation"])
|
|
331
|
+
elif "payment" in message.lower():
|
|
332
|
+
queries.extend(["payment processing", "transaction", "billing"])
|
|
333
|
+
elif "user" in message.lower():
|
|
334
|
+
queries.extend(["user management", "authentication", "registration"])
|
|
335
|
+
|
|
336
|
+
elif intent == "refactor":
|
|
337
|
+
# Extract target symbols
|
|
338
|
+
# E.g., "Refactor payment processing" -> ["payment processing", "payment service"]
|
|
339
|
+
match = re.search(r'(?:refactor|extract|rename)\s+(.+?)(?:\s+into|\s+to|$)', message, re.IGNORECASE)
|
|
340
|
+
if match:
|
|
341
|
+
target = match.group(1).strip()
|
|
342
|
+
queries.append(target)
|
|
343
|
+
queries.append(f"{target} service")
|
|
344
|
+
|
|
345
|
+
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
|
|
351
|
+
|
|
352
|
+
# Fallback: use entire message
|
|
353
|
+
if not queries:
|
|
354
|
+
queries.append(message)
|
|
355
|
+
|
|
356
|
+
return queries[:3] # Max 3 queries
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def count_tokens(text: str) -> int:
|
|
360
|
+
"""Approximate token count.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
text: Text to count tokens for
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Approximate token count
|
|
367
|
+
"""
|
|
368
|
+
# Rough approximation: 1 token ≈ 4 characters
|
|
369
|
+
return len(text) // 4
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def assemble_context_for_llm(
|
|
373
|
+
user_message: str,
|
|
374
|
+
session: ChatSession,
|
|
375
|
+
rag_retriever: RAGRetriever,
|
|
376
|
+
system_prompt: str,
|
|
377
|
+
max_tokens: int = 8000
|
|
378
|
+
) -> List[Dict[str, str]]:
|
|
379
|
+
"""Assemble optimized context for LLM within token budget.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
user_message: Current user message
|
|
383
|
+
session: Chat session with history
|
|
384
|
+
rag_retriever: RAG retriever for code search
|
|
385
|
+
system_prompt: System prompt
|
|
386
|
+
max_tokens: Maximum total tokens
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
List of message dicts for LLM
|
|
390
|
+
"""
|
|
391
|
+
context = []
|
|
392
|
+
token_count = 0
|
|
393
|
+
|
|
394
|
+
# 1. System prompt (always included)
|
|
395
|
+
context.append({"role": "system", "content": system_prompt})
|
|
396
|
+
token_count += count_tokens(system_prompt)
|
|
397
|
+
|
|
398
|
+
# 2. Detect intent
|
|
399
|
+
intent = detect_intent(user_message)
|
|
400
|
+
|
|
401
|
+
# 3. Extract queries and retrieve code
|
|
402
|
+
queries = extract_queries_from_message(user_message, intent)
|
|
403
|
+
code_snippets = []
|
|
404
|
+
|
|
405
|
+
for query in queries:
|
|
406
|
+
results = rag_retriever.search(query, top_k=3)
|
|
407
|
+
# Filter by minimum score
|
|
408
|
+
filtered = [r for r in results if r.score >= 0.15]
|
|
409
|
+
code_snippets.extend(filtered)
|
|
410
|
+
|
|
411
|
+
# Deduplicate by node_id
|
|
412
|
+
seen = set()
|
|
413
|
+
unique_snippets = []
|
|
414
|
+
for snippet in sorted(code_snippets, key=lambda x: x.score, reverse=True):
|
|
415
|
+
if snippet.node_id not in seen:
|
|
416
|
+
seen.add(snippet.node_id)
|
|
417
|
+
unique_snippets.append(snippet)
|
|
418
|
+
|
|
419
|
+
# Limit to top 10
|
|
420
|
+
unique_snippets = unique_snippets[:10]
|
|
421
|
+
|
|
422
|
+
# 4. Add RAG context (high priority)
|
|
423
|
+
if unique_snippets:
|
|
424
|
+
rag_context = format_code_snippets(unique_snippets)
|
|
425
|
+
rag_tokens = count_tokens(rag_context)
|
|
426
|
+
|
|
427
|
+
if token_count + rag_tokens < max_tokens - 2000:
|
|
428
|
+
context.append({
|
|
429
|
+
"role": "system",
|
|
430
|
+
"content": f"Relevant code from codebase:\n\n{rag_context}"
|
|
431
|
+
})
|
|
432
|
+
token_count += rag_tokens
|
|
433
|
+
|
|
434
|
+
# 5. Add pending proposals if exist
|
|
435
|
+
if session.pending_proposals:
|
|
436
|
+
proposal_text = format_proposals(session.pending_proposals)
|
|
437
|
+
proposal_tokens = count_tokens(proposal_text)
|
|
438
|
+
|
|
439
|
+
if token_count + proposal_tokens < max_tokens - 1500:
|
|
440
|
+
context.append({
|
|
441
|
+
"role": "system",
|
|
442
|
+
"content": f"Pending proposals:\n\n{proposal_text}"
|
|
443
|
+
})
|
|
444
|
+
token_count += proposal_tokens
|
|
445
|
+
|
|
446
|
+
# 6. Add conversation history (compressed)
|
|
447
|
+
conv_memory = ConversationMemory(max_recent=3)
|
|
448
|
+
conv_context = conv_memory.get_context_for_llm(
|
|
449
|
+
session,
|
|
450
|
+
token_budget=max_tokens - token_count - 500
|
|
451
|
+
)
|
|
452
|
+
context.extend(conv_context)
|
|
453
|
+
|
|
454
|
+
# 7. Add current user message
|
|
455
|
+
context.append({"role": "user", "content": user_message})
|
|
456
|
+
|
|
457
|
+
return context
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def format_code_snippets(snippets: List) -> str:
|
|
461
|
+
"""Format code snippets for LLM context.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
snippets: List of SearchResult objects
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Formatted string
|
|
468
|
+
"""
|
|
469
|
+
blocks = []
|
|
470
|
+
|
|
471
|
+
for snippet in snippets:
|
|
472
|
+
blocks.append(
|
|
473
|
+
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```"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
return "\n\n".join(blocks)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def format_proposals(proposals: List[CodeProposal]) -> str:
|
|
483
|
+
"""Format pending proposals for LLM context.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
proposals: List of CodeProposal objects
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Formatted string
|
|
490
|
+
"""
|
|
491
|
+
blocks = []
|
|
492
|
+
|
|
493
|
+
for i, proposal in enumerate(proposals, 1):
|
|
494
|
+
blocks.append(
|
|
495
|
+
f"Proposal {i}: {proposal.description}\n"
|
|
496
|
+
f"Files affected: {proposal.num_files_changed}\n"
|
|
497
|
+
f"Status: Pending user approval"
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
return "\n\n".join(blocks)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""CrewAI agents for CodeGraph CLI — specialized multi-agent system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, List
|
|
6
|
+
|
|
7
|
+
from crewai import Agent
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .crew_tools import create_tools
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_file_ops_agent(tools: list, llm, project_context: str = "") -> Agent:
|
|
14
|
+
"""File system agent — reads, writes, patches, and manages files + backups."""
|
|
15
|
+
ctx = f"\n\nCurrent Project: {project_context}" if project_context else ""
|
|
16
|
+
return Agent(
|
|
17
|
+
role="File System Engineer",
|
|
18
|
+
goal=(
|
|
19
|
+
"Handle all file operations: read files, write new files, patch existing code, "
|
|
20
|
+
"delete files, and manage backups/rollbacks. Always create backups before modifying files."
|
|
21
|
+
),
|
|
22
|
+
backstory=(
|
|
23
|
+
"You are an expert file system engineer. You navigate project directories, "
|
|
24
|
+
"read source code, write and patch files precisely. You ALWAYS create a backup "
|
|
25
|
+
"before modifying any file so changes can be rolled back. When writing code, you "
|
|
26
|
+
"produce complete, working, well-formatted files. When patching, you use exact "
|
|
27
|
+
"text matching to make surgical edits. You use the file_tree tool to understand "
|
|
28
|
+
f"project structure before making changes.{ctx}"
|
|
29
|
+
),
|
|
30
|
+
tools=tools,
|
|
31
|
+
llm=llm,
|
|
32
|
+
verbose=False,
|
|
33
|
+
allow_delegation=False,
|
|
34
|
+
max_iter=15,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_code_gen_agent(tools: list, llm, project_context: str = "") -> Agent:
|
|
39
|
+
"""Code generation & refactoring agent — writes, modifies, and improves code."""
|
|
40
|
+
ctx = f"\n\nCurrent Project: {project_context}" if project_context else ""
|
|
41
|
+
return Agent(
|
|
42
|
+
role="Senior Software Developer",
|
|
43
|
+
goal=(
|
|
44
|
+
"Generate high-quality code, implement features, refactor existing code, "
|
|
45
|
+
"and fix bugs. Read existing code first to match project style. Always use "
|
|
46
|
+
"patch_file for edits and write_file for new files."
|
|
47
|
+
),
|
|
48
|
+
backstory=(
|
|
49
|
+
"You are a senior software developer with deep expertise in Python, JavaScript, "
|
|
50
|
+
"TypeScript, and system design. You follow these principles:\n"
|
|
51
|
+
"1. ALWAYS read the existing file before modifying it\n"
|
|
52
|
+
"2. Use patch_file for targeted edits (preferred) or write_file for new/rewritten files\n"
|
|
53
|
+
"3. Match the existing code style, imports, and patterns\n"
|
|
54
|
+
"4. Include proper error handling, type hints, and docstrings\n"
|
|
55
|
+
"5. Use search_code and grep_in_project to understand how code is used before changing it\n"
|
|
56
|
+
"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}"
|
|
58
|
+
),
|
|
59
|
+
tools=tools,
|
|
60
|
+
llm=llm,
|
|
61
|
+
verbose=False,
|
|
62
|
+
allow_delegation=False,
|
|
63
|
+
max_iter=20,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_code_analysis_agent(tools: list, llm, project_context: str = "") -> Agent:
|
|
68
|
+
"""Code analysis agent — searches, understands, and explains code."""
|
|
69
|
+
ctx = f"\n\nCurrent Project: {project_context}" if project_context else ""
|
|
70
|
+
return Agent(
|
|
71
|
+
role="Code Intelligence Analyst",
|
|
72
|
+
goal=(
|
|
73
|
+
"Search, analyze, and explain code. Find relevant functions, trace dependencies, "
|
|
74
|
+
"understand how things connect, and provide clear explanations."
|
|
75
|
+
),
|
|
76
|
+
backstory=(
|
|
77
|
+
"You are a code analysis expert. You use semantic search (search_code) to find "
|
|
78
|
+
"relevant code by meaning, and grep_in_project to find exact text patterns. "
|
|
79
|
+
"You read files to understand implementation details, trace call chains, and "
|
|
80
|
+
"explain complex code clearly. When analyzing:\n"
|
|
81
|
+
"1. Use search_code for finding code by concept/meaning\n"
|
|
82
|
+
"2. Use grep_in_project for finding exact function/class usage\n"
|
|
83
|
+
"3. Use read_file to get full context of interesting results\n"
|
|
84
|
+
"4. Use get_project_summary and file_tree for structural overview\n"
|
|
85
|
+
f"5. Provide clear, specific answers — never guess{ctx}"
|
|
86
|
+
),
|
|
87
|
+
tools=tools,
|
|
88
|
+
llm=llm,
|
|
89
|
+
verbose=False,
|
|
90
|
+
allow_delegation=False,
|
|
91
|
+
max_iter=15,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def create_coordinator_agent(llm, project_context: str = "") -> Agent:
|
|
96
|
+
"""Coordinator agent — routes tasks to the right specialist."""
|
|
97
|
+
ctx = f" Current Project: {project_context}." if project_context else ""
|
|
98
|
+
return Agent(
|
|
99
|
+
role="Project Coordinator",
|
|
100
|
+
goal=(
|
|
101
|
+
"Understand user requests and coordinate specialist agents. Route file operations "
|
|
102
|
+
"to File System Engineer, code changes to Senior Software Developer, and analysis "
|
|
103
|
+
"to Code Intelligence Analyst."
|
|
104
|
+
),
|
|
105
|
+
backstory=(
|
|
106
|
+
f"You are a project coordinator managing a team of AI specialists.{ctx}\n\n"
|
|
107
|
+
"Your team:\n"
|
|
108
|
+
"• File System Engineer — reads/writes/patches files, manages backups & rollbacks\n"
|
|
109
|
+
"• Senior Software Developer — generates code, implements features, refactors\n"
|
|
110
|
+
"• Code Intelligence Analyst — searches code, analyzes dependencies, explains logic\n\n"
|
|
111
|
+
"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"
|
|
114
|
+
"3. For 'write code' / 'add feature' / 'fix bug' / 'refactor' → delegate to Senior Software Developer\n"
|
|
115
|
+
"4. For 'find' / 'search' / 'explain' / 'how does X work' → delegate to Code Intelligence Analyst\n"
|
|
116
|
+
"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"
|
|
118
|
+
),
|
|
119
|
+
llm=llm,
|
|
120
|
+
verbose=False,
|
|
121
|
+
allow_delegation=True,
|
|
122
|
+
max_iter=10,
|
|
123
|
+
)
|