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.
Files changed (43) hide show
  1. codegraph_cli/__init__.py +4 -0
  2. codegraph_cli/agents.py +191 -0
  3. codegraph_cli/bug_detector.py +386 -0
  4. codegraph_cli/chat_agent.py +352 -0
  5. codegraph_cli/chat_session.py +220 -0
  6. codegraph_cli/cli.py +330 -0
  7. codegraph_cli/cli_chat.py +367 -0
  8. codegraph_cli/cli_diagnose.py +133 -0
  9. codegraph_cli/cli_refactor.py +230 -0
  10. codegraph_cli/cli_setup.py +470 -0
  11. codegraph_cli/cli_test.py +177 -0
  12. codegraph_cli/cli_v2.py +267 -0
  13. codegraph_cli/codegen_agent.py +265 -0
  14. codegraph_cli/config.py +31 -0
  15. codegraph_cli/config_manager.py +341 -0
  16. codegraph_cli/context_manager.py +500 -0
  17. codegraph_cli/crew_agents.py +123 -0
  18. codegraph_cli/crew_chat.py +159 -0
  19. codegraph_cli/crew_tools.py +497 -0
  20. codegraph_cli/diff_engine.py +265 -0
  21. codegraph_cli/embeddings.py +241 -0
  22. codegraph_cli/graph_export.py +144 -0
  23. codegraph_cli/llm.py +642 -0
  24. codegraph_cli/models.py +47 -0
  25. codegraph_cli/models_v2.py +185 -0
  26. codegraph_cli/orchestrator.py +49 -0
  27. codegraph_cli/parser.py +800 -0
  28. codegraph_cli/performance_analyzer.py +223 -0
  29. codegraph_cli/project_context.py +230 -0
  30. codegraph_cli/rag.py +200 -0
  31. codegraph_cli/refactor_agent.py +452 -0
  32. codegraph_cli/security_scanner.py +366 -0
  33. codegraph_cli/storage.py +390 -0
  34. codegraph_cli/templates/graph_interactive.html +257 -0
  35. codegraph_cli/testgen_agent.py +316 -0
  36. codegraph_cli/validation_engine.py +285 -0
  37. codegraph_cli/vector_store.py +293 -0
  38. codegraph_cli-2.0.0.dist-info/METADATA +318 -0
  39. codegraph_cli-2.0.0.dist-info/RECORD +43 -0
  40. codegraph_cli-2.0.0.dist-info/WHEEL +5 -0
  41. codegraph_cli-2.0.0.dist-info/entry_points.txt +2 -0
  42. codegraph_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
  43. 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
+ )