nexus-dev 3.2.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.

Potentially problematic release.


This version of nexus-dev might be problematic. Click here for more details.

Files changed (48) hide show
  1. nexus_dev/__init__.py +4 -0
  2. nexus_dev/agent_templates/__init__.py +26 -0
  3. nexus_dev/agent_templates/api_designer.yaml +26 -0
  4. nexus_dev/agent_templates/code_reviewer.yaml +26 -0
  5. nexus_dev/agent_templates/debug_detective.yaml +26 -0
  6. nexus_dev/agent_templates/doc_writer.yaml +26 -0
  7. nexus_dev/agent_templates/performance_optimizer.yaml +26 -0
  8. nexus_dev/agent_templates/refactor_architect.yaml +26 -0
  9. nexus_dev/agent_templates/security_auditor.yaml +26 -0
  10. nexus_dev/agent_templates/test_engineer.yaml +26 -0
  11. nexus_dev/agents/__init__.py +20 -0
  12. nexus_dev/agents/agent_config.py +97 -0
  13. nexus_dev/agents/agent_executor.py +197 -0
  14. nexus_dev/agents/agent_manager.py +104 -0
  15. nexus_dev/agents/prompt_factory.py +91 -0
  16. nexus_dev/chunkers/__init__.py +168 -0
  17. nexus_dev/chunkers/base.py +202 -0
  18. nexus_dev/chunkers/docs_chunker.py +291 -0
  19. nexus_dev/chunkers/java_chunker.py +343 -0
  20. nexus_dev/chunkers/javascript_chunker.py +312 -0
  21. nexus_dev/chunkers/python_chunker.py +308 -0
  22. nexus_dev/cli.py +1673 -0
  23. nexus_dev/config.py +253 -0
  24. nexus_dev/database.py +558 -0
  25. nexus_dev/embeddings.py +585 -0
  26. nexus_dev/gateway/__init__.py +10 -0
  27. nexus_dev/gateway/connection_manager.py +348 -0
  28. nexus_dev/github_importer.py +247 -0
  29. nexus_dev/mcp_client.py +281 -0
  30. nexus_dev/mcp_config.py +184 -0
  31. nexus_dev/schemas/mcp_config_schema.json +166 -0
  32. nexus_dev/server.py +1866 -0
  33. nexus_dev/templates/pre-commit-hook +33 -0
  34. nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/__init__.py +26 -0
  35. nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/api_designer.yaml +26 -0
  36. nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/code_reviewer.yaml +26 -0
  37. nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/debug_detective.yaml +26 -0
  38. nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/doc_writer.yaml +26 -0
  39. nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/performance_optimizer.yaml +26 -0
  40. nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/refactor_architect.yaml +26 -0
  41. nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/security_auditor.yaml +26 -0
  42. nexus_dev-3.2.0.data/data/nexus_dev/agent_templates/test_engineer.yaml +26 -0
  43. nexus_dev-3.2.0.data/data/nexus_dev/templates/pre-commit-hook +33 -0
  44. nexus_dev-3.2.0.dist-info/METADATA +636 -0
  45. nexus_dev-3.2.0.dist-info/RECORD +48 -0
  46. nexus_dev-3.2.0.dist-info/WHEEL +4 -0
  47. nexus_dev-3.2.0.dist-info/entry_points.txt +12 -0
  48. nexus_dev-3.2.0.dist-info/licenses/LICENSE +21 -0
nexus_dev/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Nexus-Dev: MCP Server for Persistent AI Coding Assistant Memory."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Marco Mornati"
@@ -0,0 +1,26 @@
1
+ """Agent templates package."""
2
+
3
+ from pathlib import Path
4
+
5
+ TEMPLATES_DIR = Path(__file__).parent
6
+
7
+
8
+ def get_template_path(template_name: str) -> Path:
9
+ """Get the path to a template file.
10
+
11
+ Args:
12
+ template_name: Name of the template (without .yaml extension).
13
+
14
+ Returns:
15
+ Path to the template YAML file.
16
+ """
17
+ return TEMPLATES_DIR / f"{template_name}.yaml"
18
+
19
+
20
+ def list_templates() -> list[str]:
21
+ """List all available template names.
22
+
23
+ Returns:
24
+ List of template names (without .yaml extension).
25
+ """
26
+ return [p.stem for p in TEMPLATES_DIR.glob("*.yaml") if not p.name.startswith("_")]
@@ -0,0 +1,26 @@
1
+ name: "api_designer"
2
+ display_name: "API Designer"
3
+ description: "Reviews and designs REST/GraphQL APIs following best practices."
4
+
5
+ profile:
6
+ role: "API Architect"
7
+ goal: "Design RESTful and GraphQL APIs that are intuitive, consistent, and follow industry best practices"
8
+ backstory: |
9
+ API architect with extensive experience in designing scalable, developer-friendly APIs.
10
+ Expert in REST principles, GraphQL schema design, API versioning, and documentation.
11
+ Has designed APIs for companies ranging from startups to enterprises. Believes that
12
+ great APIs are predictable, consistent, and self-explanatory.
13
+ tone: "Professional, pragmatic, and standards-focused"
14
+
15
+ memory:
16
+ enabled: true
17
+ rag_limit: 6
18
+ search_types: ["code", "documentation", "lesson"]
19
+
20
+ tools: []
21
+
22
+ llm_config:
23
+ model_hint: "claude-sonnet-4.5"
24
+ fallback_hints: ["gpt-5.2", "gemini-3-pro", "auto"]
25
+ temperature: 0.4
26
+ max_tokens: 5000
@@ -0,0 +1,26 @@
1
+ name: "code_reviewer"
2
+ display_name: "Code Reviewer"
3
+ description: "Analyzes code for bugs, security issues, and best practices."
4
+
5
+ profile:
6
+ role: "Senior Code Reviewer"
7
+ goal: "Identify bugs, security vulnerabilities, and suggest improvements following best practices"
8
+ backstory: |
9
+ Expert software engineer with 15+ years of experience in code quality assurance.
10
+ Specialized in identifying subtle bugs, security vulnerabilities, and architectural issues.
11
+ Familiar with SOLID principles, design patterns, and language-specific best practices.
12
+ Provides constructive feedback that helps developers learn and improve.
13
+ tone: "Professional, constructive, and educational"
14
+
15
+ memory:
16
+ enabled: true
17
+ rag_limit: 8
18
+ search_types: ["code", "documentation", "lesson"]
19
+
20
+ tools: []
21
+
22
+ llm_config:
23
+ model_hint: "claude-sonnet-4.5"
24
+ fallback_hints: ["gpt-5.2-codex", "gemini-3-pro", "auto"]
25
+ temperature: 0.3
26
+ max_tokens: 4000
@@ -0,0 +1,26 @@
1
+ name: "debug_detective"
2
+ display_name: "Debug Detective"
3
+ description: "Investigates bugs, analyzes stack traces, and proposes targeted fixes."
4
+
5
+ profile:
6
+ role: "Debugging Specialist"
7
+ goal: "Analyze errors, trace root causes, and propose precise, targeted fixes"
8
+ backstory: |
9
+ Veteran debugger with a methodical approach to solving cryptic errors.
10
+ Expert in reading stack traces, understanding memory issues, async debugging,
11
+ and race conditions. Has a systematic process: reproduce, isolate, understand, fix.
12
+ Known for finding the needle in the haystack.
13
+ tone: "Analytical, patient, and thorough"
14
+
15
+ memory:
16
+ enabled: true
17
+ rag_limit: 10
18
+ search_types: ["code", "lesson", "documentation"]
19
+
20
+ tools: []
21
+
22
+ llm_config:
23
+ model_hint: "claude-sonnet-4.5"
24
+ fallback_hints: ["gemini-3-deep-think", "gpt-5.2-codex", "auto"]
25
+ temperature: 0.2
26
+ max_tokens: 6000
@@ -0,0 +1,26 @@
1
+ name: "doc_writer"
2
+ display_name: "Documentation Writer"
3
+ description: "Creates and improves technical documentation, API docs, and README files."
4
+
5
+ profile:
6
+ role: "Technical Writer"
7
+ goal: "Create clear, comprehensive documentation that helps developers understand and use code effectively"
8
+ backstory: |
9
+ Professional technical writer with expertise in developer documentation.
10
+ Skilled at explaining complex concepts in simple terms, creating API references,
11
+ writing tutorials, and maintaining README files. Believes good documentation
12
+ is as important as good code.
13
+ tone: "Clear, friendly, and helpful"
14
+
15
+ memory:
16
+ enabled: true
17
+ rag_limit: 6
18
+ search_types: ["code", "documentation", "lesson"]
19
+
20
+ tools: []
21
+
22
+ llm_config:
23
+ model_hint: "claude-opus-4.5"
24
+ fallback_hints: ["claude-sonnet-4.5", "gpt-5.2", "auto"]
25
+ temperature: 0.6
26
+ max_tokens: 6000
@@ -0,0 +1,26 @@
1
+ name: "performance_optimizer"
2
+ display_name: "Performance Optimizer"
3
+ description: "Identifies performance bottlenecks and recommends optimizations."
4
+
5
+ profile:
6
+ role: "Performance Engineer"
7
+ goal: "Identify performance bottlenecks, optimize code for speed and efficiency, and reduce resource consumption"
8
+ backstory: |
9
+ Performance engineering specialist with expertise in profiling, benchmarking, and optimization.
10
+ Skilled at identifying N+1 queries, memory leaks, inefficient algorithms, and resource bottlenecks.
11
+ Has optimized applications handling millions of requests per day. Believes in measuring first,
12
+ then optimizing based on data.
13
+ tone: "Data-driven, analytical, and results-oriented"
14
+
15
+ memory:
16
+ enabled: true
17
+ rag_limit: 8
18
+ search_types: ["code", "documentation", "lesson"]
19
+
20
+ tools: []
21
+
22
+ llm_config:
23
+ model_hint: "gemini-3-pro"
24
+ fallback_hints: ["claude-sonnet-4.5", "gpt-5.2-codex", "auto"]
25
+ temperature: 0.3
26
+ max_tokens: 5000
@@ -0,0 +1,26 @@
1
+ name: "refactor_architect"
2
+ display_name: "Refactor Architect"
3
+ description: "Restructures code for better maintainability, performance, and design patterns."
4
+
5
+ profile:
6
+ role: "Refactoring Expert"
7
+ goal: "Restructure code to improve readability, maintainability, and performance without changing behavior"
8
+ backstory: |
9
+ Software architect with deep expertise in design patterns and clean code principles.
10
+ Specializes in identifying code smells, reducing complexity, and applying SOLID principles.
11
+ Has successfully refactored legacy codebases for Fortune 500 companies. Believes that
12
+ well-structured code is easier to understand, test, and extend.
13
+ tone: "Strategic, clear, and methodical"
14
+
15
+ memory:
16
+ enabled: true
17
+ rag_limit: 10
18
+ search_types: ["code", "documentation", "lesson"]
19
+
20
+ tools: []
21
+
22
+ llm_config:
23
+ model_hint: "gemini-3-deep-think"
24
+ fallback_hints: ["claude-opus-4.5", "claude-sonnet-4.5", "auto"]
25
+ temperature: 0.4
26
+ max_tokens: 6000
@@ -0,0 +1,26 @@
1
+ name: "security_auditor"
2
+ display_name: "Security Auditor"
3
+ description: "Identifies security vulnerabilities and recommends fixes based on OWASP guidelines."
4
+
5
+ profile:
6
+ role: "Security Analyst"
7
+ goal: "Identify security vulnerabilities, recommend fixes, and ensure code follows security best practices"
8
+ backstory: |
9
+ Cybersecurity expert with focus on application security and secure coding practices.
10
+ Familiar with OWASP Top 10, common vulnerabilities (SQL injection, XSS, CSRF, etc.),
11
+ and security frameworks. Has experience with security audits, penetration testing,
12
+ and secure code reviews for financial and healthcare applications.
13
+ tone: "Serious, precise, and security-focused"
14
+
15
+ memory:
16
+ enabled: true
17
+ rag_limit: 7
18
+ search_types: ["code", "documentation", "lesson"]
19
+
20
+ tools: []
21
+
22
+ llm_config:
23
+ model_hint: "claude-opus-4.5"
24
+ fallback_hints: ["claude-sonnet-4.5", "gpt-5.2", "auto"]
25
+ temperature: 0.2
26
+ max_tokens: 5000
@@ -0,0 +1,26 @@
1
+ name: "test_engineer"
2
+ display_name: "Test Engineer"
3
+ description: "Generates comprehensive test cases and improves test coverage."
4
+
5
+ profile:
6
+ role: "QA Engineer"
7
+ goal: "Generate comprehensive test cases covering edge cases, improve test coverage, and ensure code quality"
8
+ backstory: |
9
+ Quality assurance specialist with expertise in TDD and BDD methodologies.
10
+ Skilled in pytest, Jest, JUnit, and other testing frameworks. Believes in testing
11
+ the unhappy paths as much as the happy ones. Writes tests that are readable,
12
+ maintainable, and catch real bugs.
13
+ tone: "Methodical, detail-oriented, and thorough"
14
+
15
+ memory:
16
+ enabled: true
17
+ rag_limit: 5
18
+ search_types: ["code", "documentation", "lesson"]
19
+
20
+ tools: []
21
+
22
+ llm_config:
23
+ model_hint: "gpt-5.2-codex"
24
+ fallback_hints: ["claude-sonnet-4.5", "gemini-3-pro", "auto"]
25
+ temperature: 0.4
26
+ max_tokens: 4000
@@ -0,0 +1,20 @@
1
+ """Nexus-Dev Custom Agents Package.
2
+
3
+ This package provides functionality for defining and executing custom AI agents
4
+ that leverage the project's RAG (lessons/docs) and MCP tools.
5
+ """
6
+
7
+ from .agent_config import AgentConfig, AgentMemory, AgentProfile, LLMConfig
8
+ from .agent_executor import AgentExecutor
9
+ from .agent_manager import AgentManager
10
+ from .prompt_factory import PromptFactory
11
+
12
+ __all__ = [
13
+ "AgentConfig",
14
+ "AgentProfile",
15
+ "AgentMemory",
16
+ "LLMConfig",
17
+ "AgentManager",
18
+ "AgentExecutor",
19
+ "PromptFactory",
20
+ ]
@@ -0,0 +1,97 @@
1
+ """Pydantic models for agent configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class AgentProfile(BaseModel):
11
+ """Agent persona definition.
12
+
13
+ Attributes:
14
+ role: Role title (e.g., 'Code Reviewer').
15
+ goal: Primary objective of the agent.
16
+ backstory: Agent's background and expertise.
17
+ tone: Communication style.
18
+ """
19
+
20
+ role: str = Field(..., description="Role title (e.g., 'Code Reviewer')")
21
+ goal: str = Field(..., description="Primary objective")
22
+ backstory: str = Field(..., description="Agent's background and expertise")
23
+ tone: str = Field(default="Professional and direct")
24
+
25
+
26
+ class AgentMemory(BaseModel):
27
+ """RAG configuration for the agent.
28
+
29
+ Attributes:
30
+ enabled: Whether to enable project RAG search.
31
+ rag_limit: Maximum number of context chunks to retrieve.
32
+ search_types: Document types to include in RAG search.
33
+ """
34
+
35
+ enabled: bool = Field(default=True, description="Enable project RAG search")
36
+ rag_limit: int = Field(default=5, ge=1, le=20, description="Max context chunks")
37
+ search_types: list[Literal["code", "documentation", "lesson"]] = Field(
38
+ default=["code", "documentation", "lesson"],
39
+ description="Document types to include in RAG",
40
+ )
41
+
42
+
43
+ class LLMConfig(BaseModel):
44
+ """Model hint for MCP Sampling.
45
+
46
+ The model_hint is sent to the IDE as a preference. The IDE may use a different
47
+ model based on its configuration and available models.
48
+
49
+ Attributes:
50
+ model_hint: Suggested model name for the IDE.
51
+ fallback_hints: Alternative models if primary is unavailable.
52
+ temperature: Sampling temperature.
53
+ max_tokens: Maximum tokens in response.
54
+ """
55
+
56
+ model_hint: str = Field(
57
+ default="claude-sonnet-4.5",
58
+ description="Model preference sent to IDE (hint only)",
59
+ )
60
+ fallback_hints: list[str] = Field(
61
+ default_factory=lambda: ["auto"],
62
+ description="Fallback models if primary unavailable",
63
+ )
64
+ temperature: float = Field(default=0.5, ge=0.0, le=2.0)
65
+ max_tokens: int = Field(default=4000, ge=100, le=32000)
66
+
67
+
68
+ class AgentConfig(BaseModel):
69
+ """Complete agent configuration.
70
+
71
+ This model represents a complete agent definition loaded from a YAML file.
72
+ It includes the agent's identity, memory settings, tool access, and LLM preferences.
73
+
74
+ Attributes:
75
+ name: Internal identifier (lowercase with underscores).
76
+ display_name: Human-readable name for the agent.
77
+ description: Tool description for AI discovery.
78
+ profile: Agent persona definition.
79
+ memory: RAG configuration.
80
+ tools: List of allowed MCP tool names (empty = all tools).
81
+ llm_config: Model preferences for MCP Sampling.
82
+ """
83
+
84
+ name: str = Field(
85
+ ...,
86
+ pattern=r"^[a-z][a-z0-9_]*$",
87
+ description="Internal ID (lowercase, underscores allowed)",
88
+ )
89
+ display_name: str = Field(..., description="Human-readable name")
90
+ description: str = Field(..., description="Tool description for AI discovery")
91
+ profile: AgentProfile
92
+ memory: AgentMemory = Field(default_factory=AgentMemory)
93
+ tools: list[str] = Field(
94
+ default_factory=list,
95
+ description="Allowed MCP tool names (empty = all project tools)",
96
+ )
97
+ llm_config: LLMConfig = Field(default_factory=LLMConfig)
@@ -0,0 +1,197 @@
1
+ """Execute agent tasks using MCP Sampling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from mcp.types import (
9
+ CreateMessageRequestParams,
10
+ ModelHint,
11
+ ModelPreferences,
12
+ SamplingMessage,
13
+ TextContent,
14
+ )
15
+
16
+ from .agent_config import AgentConfig
17
+ from .prompt_factory import PromptFactory
18
+
19
+ if TYPE_CHECKING:
20
+ from mcp.server.fastmcp import FastMCP
21
+
22
+ from ..database import NexusDatabase
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class AgentExecutor:
28
+ """Orchestrate agent execution with RAG and MCP Sampling.
29
+
30
+ This executor:
31
+ 1. Retrieves relevant context from the project's LanceDB (RAG)
32
+ 2. Builds a structured system prompt using PromptFactory
33
+ 3. Sends a sampling request to the IDE via MCP
34
+ 4. Returns the IDE's response
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ config: AgentConfig,
40
+ database: NexusDatabase,
41
+ mcp_server: FastMCP,
42
+ ) -> None:
43
+ """Initialize the agent executor.
44
+
45
+ Args:
46
+ config: Agent configuration from YAML.
47
+ database: NexusDatabase instance for RAG search.
48
+ mcp_server: FastMCP server instance for sampling requests.
49
+ """
50
+ self.config = config
51
+ self.database = database
52
+ self.mcp_server = mcp_server
53
+
54
+ async def execute(self, user_task: str, project_id: str | None = None) -> str:
55
+ """Execute a task with the configured agent.
56
+
57
+ The execution flow:
58
+ 1. Retrieve relevant context from RAG based on the task
59
+ 2. Build system prompt with agent persona and context
60
+ 3. Send sampling request to IDE
61
+ 4. Return the response
62
+
63
+ Args:
64
+ user_task: The task description from the user.
65
+ project_id: Optional project ID for RAG filtering.
66
+
67
+ Returns:
68
+ The agent's response text.
69
+ """
70
+ from ..database import DocumentType
71
+
72
+ # 1. RAG Retrieval
73
+ context_items: list[str] = []
74
+ if self.config.memory.enabled:
75
+ for search_type in self.config.memory.search_types:
76
+ try:
77
+ doc_type = DocumentType(search_type)
78
+ results = await self.database.search(
79
+ query=user_task,
80
+ project_id=project_id,
81
+ doc_type=doc_type,
82
+ limit=self.config.memory.rag_limit,
83
+ )
84
+ # Truncate each result to avoid context overflow
85
+ context_items.extend([r.text[:500] for r in results])
86
+ except Exception as e:
87
+ logger.warning("RAG search failed for %s: %s", search_type, e)
88
+
89
+ logger.debug(
90
+ "Agent %s retrieved %d context items for task: %s",
91
+ self.config.name,
92
+ len(context_items),
93
+ user_task[:100],
94
+ )
95
+
96
+ # 2. Build Prompt
97
+ system_prompt = PromptFactory.build(
98
+ agent=self.config,
99
+ context_items=context_items,
100
+ available_tools=self.config.tools if self.config.tools else None,
101
+ )
102
+
103
+ # 3. MCP Sampling Request
104
+ try:
105
+ # Create the sampling request parameters
106
+ model_prefs = ModelPreferences(
107
+ hints=[ModelHint(name=self.config.llm_config.model_hint)],
108
+ )
109
+
110
+ request_params = CreateMessageRequestParams(
111
+ messages=[
112
+ SamplingMessage(
113
+ role="user",
114
+ content=TextContent(type="text", text=user_task),
115
+ )
116
+ ],
117
+ systemPrompt=system_prompt,
118
+ modelPreferences=model_prefs,
119
+ maxTokens=self.config.llm_config.max_tokens,
120
+ )
121
+
122
+ # Access the session from the request context
123
+ # Note: This requires the server to be in a request context
124
+ ctx = self.mcp_server.get_context()
125
+ result = await ctx.session.create_message(
126
+ messages=request_params.messages,
127
+ system_prompt=request_params.systemPrompt,
128
+ model_preferences=request_params.modelPreferences,
129
+ max_tokens=request_params.maxTokens,
130
+ )
131
+
132
+ # 4. Extract and return response text
133
+ if hasattr(result.content, "text"):
134
+ return str(result.content.text)
135
+ return str(result.content)
136
+
137
+ except Exception as e:
138
+ error_msg = str(e)
139
+ if "does not support CreateMessage" in error_msg or "Method not found" in error_msg:
140
+ logger.warning(
141
+ "MCP Sampling not supported by client. Returning context-only response."
142
+ )
143
+ return self._create_fallback_response(user_task, context_items, system_prompt)
144
+
145
+ logger.error("MCP Sampling failed for agent %s: %s", self.config.name, e)
146
+ return f"Agent execution failed: {e}"
147
+
148
+ def _create_fallback_response(
149
+ self, user_task: str, context_items: list[str], system_prompt: str
150
+ ) -> str:
151
+ """Create a Markdown response when full agent execution is not supported.
152
+
153
+ Args:
154
+ user_task: The original task.
155
+ context_items: List of retrieved context strings.
156
+ system_prompt: The constructed system prompt.
157
+
158
+ Returns:
159
+ Formatted Markdown string.
160
+ """
161
+ response = [
162
+ f"# Agent: {self.config.display_name} (Insight Mode)",
163
+ "",
164
+ "> **Note**: Your IDE does not support fully autonomous agent "
165
+ "execution (MCP Sampling).",
166
+ "> I have retrieved the relevant context and prepared the prompt for you below.",
167
+ "",
168
+ "## 1. Retrieved Context",
169
+ f"Found {len(context_items)} relevant items in the knowledge base:",
170
+ "",
171
+ ]
172
+
173
+ if context_items:
174
+ for i, item in enumerate(context_items, 1):
175
+ # Try to extract a clean title or first line
176
+ preview = item.split("\n")[0][:80]
177
+ response.append(f"**Item {i}**: `{preview}...`")
178
+ # Indent the content for better readability in the details block
179
+ response.append("<details>")
180
+ response.append(f"<summary>View Content</summary>\n\n{item}\n")
181
+ response.append("</details>\n")
182
+ else:
183
+ response.append("*No relevant context found.*")
184
+
185
+ response.extend(
186
+ [
187
+ "",
188
+ "## 2. Recommended System Prompt",
189
+ "You can use this prompt with your own LLM:",
190
+ "",
191
+ "```text",
192
+ system_prompt,
193
+ "```",
194
+ ]
195
+ )
196
+
197
+ return "\n".join(response)
@@ -0,0 +1,104 @@
1
+ """Load and manage agent configurations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Iterator
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+ from .agent_config import AgentConfig
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class AgentManager:
17
+ """Scan and load agent YAML configurations.
18
+
19
+ This manager:
20
+ - Scans the `agents/` directory in the project root
21
+ - Loads and validates each YAML file as an AgentConfig
22
+ - Provides access to loaded agents by name
23
+ """
24
+
25
+ def __init__(self, agents_dir: Path | None = None) -> None:
26
+ """Initialize the agent manager.
27
+
28
+ Args:
29
+ agents_dir: Path to the agents directory. Defaults to `./agents/`.
30
+ """
31
+ self.agents_dir = agents_dir or Path.cwd() / "agents"
32
+ self.agents: dict[str, AgentConfig] = {}
33
+ self._load_agents()
34
+
35
+ def _load_agents(self) -> None:
36
+ """Load all valid agent configs from the agents directory.
37
+
38
+ Invalid configs are logged as warnings but don't prevent other agents
39
+ from loading.
40
+ """
41
+ if not self.agents_dir.exists():
42
+ logger.debug("Agents directory not found: %s", self.agents_dir)
43
+ return
44
+
45
+ yaml_files = list(self.agents_dir.glob("*.yaml")) + list(self.agents_dir.glob("*.yml"))
46
+
47
+ for yaml_file in yaml_files:
48
+ try:
49
+ with open(yaml_file, encoding="utf-8") as f:
50
+ data = yaml.safe_load(f)
51
+
52
+ if not data:
53
+ logger.warning("Empty agent file: %s", yaml_file.name)
54
+ continue
55
+
56
+ agent = AgentConfig(**data)
57
+ self.agents[agent.name] = agent
58
+ logger.info("Loaded agent: %s from %s", agent.name, yaml_file.name)
59
+
60
+ except yaml.YAMLError as e:
61
+ logger.warning("Invalid YAML in %s: %s", yaml_file.name, e)
62
+ except Exception as e:
63
+ logger.warning("Failed to load agent %s: %s", yaml_file.name, e)
64
+
65
+ logger.info("Loaded %d agents from %s", len(self.agents), self.agents_dir)
66
+
67
+ def reload(self) -> None:
68
+ """Reload agents from disk.
69
+
70
+ This clears the current agents and reloads from the directory.
71
+ """
72
+ self.agents.clear()
73
+ self._load_agents()
74
+
75
+ def get_agent(self, name: str) -> AgentConfig | None:
76
+ """Get agent by name.
77
+
78
+ Args:
79
+ name: The agent's internal name.
80
+
81
+ Returns:
82
+ AgentConfig if found, None otherwise.
83
+ """
84
+ return self.agents.get(name)
85
+
86
+ def list_agents(self) -> list[AgentConfig]:
87
+ """List all loaded agents.
88
+
89
+ Returns:
90
+ List of AgentConfig instances.
91
+ """
92
+ return list(self.agents.values())
93
+
94
+ def __len__(self) -> int:
95
+ """Return number of loaded agents."""
96
+ return len(self.agents)
97
+
98
+ def __iter__(self) -> Iterator[AgentConfig]:
99
+ """Iterate over loaded agents."""
100
+ return iter(self.agents.values())
101
+
102
+ def __contains__(self, name: str) -> bool:
103
+ """Check if an agent is loaded."""
104
+ return name in self.agents