nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Layer 3: Multi-Agent Council
|
|
3
|
+
|
|
4
|
+
Orchestrates multiple AI agents that specialize in different aspects:
|
|
5
|
+
- Implementer: Writes clean, efficient code
|
|
6
|
+
- Architect: Designs system structure and patterns
|
|
7
|
+
- Reviewer: Finds bugs, security issues, improvements
|
|
8
|
+
- Debugger: Diagnoses and fixes issues
|
|
9
|
+
- Optimizer: Improves performance
|
|
10
|
+
- Security: Analyzes security implications
|
|
11
|
+
- Documentation: Writes clear documentation
|
|
12
|
+
|
|
13
|
+
The council convenes for complex tasks (complexity > threshold) and
|
|
14
|
+
produces a consensus response that combines multiple perspectives.
|
|
15
|
+
|
|
16
|
+
This layer answers: "What do our AI experts think about this?"
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Dict, List, Optional, Any, Callable, Set
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
import asyncio
|
|
25
|
+
import json
|
|
26
|
+
import threading
|
|
27
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AgentRole(Enum):
|
|
33
|
+
"""Specialized agent roles in the council"""
|
|
34
|
+
IMPLEMENTER = "implementer"
|
|
35
|
+
ARCHITECT = "architect"
|
|
36
|
+
REVIEWER = "reviewer"
|
|
37
|
+
DEBUGGER = "debugger"
|
|
38
|
+
OPTIMIZER = "optimizer"
|
|
39
|
+
SECURITY = "security"
|
|
40
|
+
DOCUMENTATION = "documentation"
|
|
41
|
+
TESTER = "tester"
|
|
42
|
+
DEVOPS = "devops"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class AgentPersona:
|
|
47
|
+
"""Configuration for an AI agent's behavior and expertise"""
|
|
48
|
+
role: AgentRole
|
|
49
|
+
name: str
|
|
50
|
+
description: str
|
|
51
|
+
system_prompt: str
|
|
52
|
+
focus_areas: List[str]
|
|
53
|
+
weight: float = 1.0 # Weight in consensus voting
|
|
54
|
+
preferred_model: Optional[str] = None
|
|
55
|
+
temperature: float = 0.7
|
|
56
|
+
max_tokens: int = 2000
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Default agent personas
|
|
60
|
+
DEFAULT_AGENTS: Dict[AgentRole, AgentPersona] = {
|
|
61
|
+
AgentRole.IMPLEMENTER: AgentPersona(
|
|
62
|
+
role=AgentRole.IMPLEMENTER,
|
|
63
|
+
name="Implementer",
|
|
64
|
+
description="Expert at writing clean, efficient, maintainable code",
|
|
65
|
+
system_prompt="""You are an expert software implementer. Your focus is on:
|
|
66
|
+
- Writing clean, readable, and maintainable code
|
|
67
|
+
- Following language idioms and best practices
|
|
68
|
+
- Implementing features correctly and completely
|
|
69
|
+
- Using appropriate data structures and algorithms
|
|
70
|
+
- Writing code that is easy to test and debug
|
|
71
|
+
|
|
72
|
+
When analyzing or writing code, prioritize correctness, clarity, and simplicity.
|
|
73
|
+
Suggest concrete implementations with actual code when relevant.""",
|
|
74
|
+
focus_areas=["code_quality", "implementation", "best_practices", "readability"],
|
|
75
|
+
weight=1.0,
|
|
76
|
+
),
|
|
77
|
+
|
|
78
|
+
AgentRole.ARCHITECT: AgentPersona(
|
|
79
|
+
role=AgentRole.ARCHITECT,
|
|
80
|
+
name="Architect",
|
|
81
|
+
description="Designs system structure, patterns, and high-level solutions",
|
|
82
|
+
system_prompt="""You are a software architect. Your focus is on:
|
|
83
|
+
- System design and architecture patterns
|
|
84
|
+
- Code organization and module structure
|
|
85
|
+
- Scalability and maintainability concerns
|
|
86
|
+
- Design patterns and their appropriate use
|
|
87
|
+
- API design and interfaces
|
|
88
|
+
- Managing complexity through abstraction
|
|
89
|
+
|
|
90
|
+
When analyzing code, think about the bigger picture: how components interact,
|
|
91
|
+
where boundaries should be, and what patterns would improve the design.""",
|
|
92
|
+
focus_areas=["architecture", "design_patterns", "scalability", "modularity"],
|
|
93
|
+
weight=1.2, # Slightly higher weight for complex decisions
|
|
94
|
+
),
|
|
95
|
+
|
|
96
|
+
AgentRole.REVIEWER: AgentPersona(
|
|
97
|
+
role=AgentRole.REVIEWER,
|
|
98
|
+
name="Reviewer",
|
|
99
|
+
description="Reviews code for bugs, issues, and improvements",
|
|
100
|
+
system_prompt="""You are an expert code reviewer. Your focus is on:
|
|
101
|
+
- Finding bugs, logic errors, and edge cases
|
|
102
|
+
- Identifying code smells and anti-patterns
|
|
103
|
+
- Suggesting improvements and refactoring opportunities
|
|
104
|
+
- Checking for consistency with project style
|
|
105
|
+
- Ensuring error handling is comprehensive
|
|
106
|
+
- Validating that tests cover important cases
|
|
107
|
+
|
|
108
|
+
Be thorough but constructive. Point out issues with specific suggestions for fixes.""",
|
|
109
|
+
focus_areas=["bugs", "code_quality", "improvements", "consistency"],
|
|
110
|
+
weight=1.0,
|
|
111
|
+
),
|
|
112
|
+
|
|
113
|
+
AgentRole.DEBUGGER: AgentPersona(
|
|
114
|
+
role=AgentRole.DEBUGGER,
|
|
115
|
+
name="Debugger",
|
|
116
|
+
description="Expert at diagnosing and fixing bugs",
|
|
117
|
+
system_prompt="""You are an expert debugger. Your focus is on:
|
|
118
|
+
- Analyzing error messages and stack traces
|
|
119
|
+
- Identifying root causes of bugs
|
|
120
|
+
- Understanding unexpected behavior
|
|
121
|
+
- Tracing execution flow and state
|
|
122
|
+
- Suggesting targeted fixes with minimal side effects
|
|
123
|
+
- Adding logging/debugging aids when helpful
|
|
124
|
+
|
|
125
|
+
When debugging, be systematic: hypothesize, verify, fix. Explain your reasoning.""",
|
|
126
|
+
focus_areas=["debugging", "error_analysis", "root_cause", "fixes"],
|
|
127
|
+
weight=1.1, # Higher weight for debugging tasks
|
|
128
|
+
),
|
|
129
|
+
|
|
130
|
+
AgentRole.OPTIMIZER: AgentPersona(
|
|
131
|
+
role=AgentRole.OPTIMIZER,
|
|
132
|
+
name="Optimizer",
|
|
133
|
+
description="Focuses on performance and efficiency improvements",
|
|
134
|
+
system_prompt="""You are a performance optimization expert. Your focus is on:
|
|
135
|
+
- Identifying performance bottlenecks
|
|
136
|
+
- Algorithm and data structure optimization
|
|
137
|
+
- Memory usage and allocation patterns
|
|
138
|
+
- I/O and database query optimization
|
|
139
|
+
- Caching strategies and memoization
|
|
140
|
+
- Profiling and benchmarking suggestions
|
|
141
|
+
|
|
142
|
+
When optimizing, consider trade-offs between readability and performance.
|
|
143
|
+
Only suggest optimizations that provide meaningful improvements.""",
|
|
144
|
+
focus_areas=["performance", "optimization", "efficiency", "memory"],
|
|
145
|
+
weight=0.9,
|
|
146
|
+
),
|
|
147
|
+
|
|
148
|
+
AgentRole.SECURITY: AgentPersona(
|
|
149
|
+
role=AgentRole.SECURITY,
|
|
150
|
+
name="Security Analyst",
|
|
151
|
+
description="Analyzes security implications and vulnerabilities",
|
|
152
|
+
system_prompt="""You are a security expert. Your focus is on:
|
|
153
|
+
- Identifying security vulnerabilities (OWASP Top 10, etc.)
|
|
154
|
+
- Input validation and sanitization
|
|
155
|
+
- Authentication and authorization issues
|
|
156
|
+
- Injection attacks (SQL, XSS, command injection)
|
|
157
|
+
- Secrets management and exposure
|
|
158
|
+
- Secure coding practices
|
|
159
|
+
|
|
160
|
+
When reviewing code, think like an attacker. Point out vulnerabilities with
|
|
161
|
+
severity levels and specific remediation steps.""",
|
|
162
|
+
focus_areas=["security", "vulnerabilities", "authentication", "validation"],
|
|
163
|
+
weight=1.3, # High weight for security concerns
|
|
164
|
+
),
|
|
165
|
+
|
|
166
|
+
AgentRole.DOCUMENTATION: AgentPersona(
|
|
167
|
+
role=AgentRole.DOCUMENTATION,
|
|
168
|
+
name="Documentarian",
|
|
169
|
+
description="Creates clear documentation and explanations",
|
|
170
|
+
system_prompt="""You are a technical writer and documentation expert. Your focus is on:
|
|
171
|
+
- Writing clear, comprehensive documentation
|
|
172
|
+
- Creating helpful docstrings and comments
|
|
173
|
+
- Explaining complex code in simple terms
|
|
174
|
+
- Writing README files and guides
|
|
175
|
+
- API documentation with examples
|
|
176
|
+
- Architecture decision records
|
|
177
|
+
|
|
178
|
+
When documenting, think about the reader: what do they need to know?
|
|
179
|
+
Provide examples and context, not just descriptions.""",
|
|
180
|
+
focus_areas=["documentation", "clarity", "examples", "explanations"],
|
|
181
|
+
weight=0.8,
|
|
182
|
+
),
|
|
183
|
+
|
|
184
|
+
AgentRole.TESTER: AgentPersona(
|
|
185
|
+
role=AgentRole.TESTER,
|
|
186
|
+
name="Tester",
|
|
187
|
+
description="Expert at testing strategies and test implementation",
|
|
188
|
+
system_prompt="""You are a testing expert. Your focus is on:
|
|
189
|
+
- Designing comprehensive test strategies
|
|
190
|
+
- Writing unit, integration, and e2e tests
|
|
191
|
+
- Identifying edge cases and boundary conditions
|
|
192
|
+
- Test coverage and quality metrics
|
|
193
|
+
- Mocking and test isolation
|
|
194
|
+
- Test-driven development practices
|
|
195
|
+
|
|
196
|
+
When analyzing code, think about how to test it effectively.
|
|
197
|
+
Suggest specific test cases that would catch bugs.""",
|
|
198
|
+
focus_areas=["testing", "test_cases", "coverage", "quality"],
|
|
199
|
+
weight=0.9,
|
|
200
|
+
),
|
|
201
|
+
|
|
202
|
+
AgentRole.DEVOPS: AgentPersona(
|
|
203
|
+
role=AgentRole.DEVOPS,
|
|
204
|
+
name="DevOps Engineer",
|
|
205
|
+
description="Expert in deployment, infrastructure, and operations",
|
|
206
|
+
system_prompt="""You are a DevOps expert. Your focus is on:
|
|
207
|
+
- CI/CD pipelines and deployment automation
|
|
208
|
+
- Infrastructure as code
|
|
209
|
+
- Container orchestration (Docker, Kubernetes)
|
|
210
|
+
- Monitoring, logging, and observability
|
|
211
|
+
- Environment configuration and secrets
|
|
212
|
+
- Scalability and reliability concerns
|
|
213
|
+
|
|
214
|
+
When analyzing code, consider operational aspects: how will this run in production?
|
|
215
|
+
What monitoring and deployment considerations apply?""",
|
|
216
|
+
focus_areas=["devops", "deployment", "infrastructure", "monitoring"],
|
|
217
|
+
weight=0.8,
|
|
218
|
+
),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class AgentResponse:
|
|
224
|
+
"""Response from a single agent"""
|
|
225
|
+
agent_role: AgentRole
|
|
226
|
+
content: str
|
|
227
|
+
confidence: float # 0.0 to 1.0
|
|
228
|
+
key_points: List[str] = field(default_factory=list)
|
|
229
|
+
suggestions: List[str] = field(default_factory=list)
|
|
230
|
+
concerns: List[str] = field(default_factory=list)
|
|
231
|
+
code_snippets: List[str] = field(default_factory=list)
|
|
232
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
233
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class CouncilSession:
|
|
238
|
+
"""A council session with multiple agent responses"""
|
|
239
|
+
session_id: str
|
|
240
|
+
task_description: str
|
|
241
|
+
agents_consulted: List[AgentRole]
|
|
242
|
+
responses: Dict[AgentRole, AgentResponse] = field(default_factory=dict)
|
|
243
|
+
consensus: Optional[str] = None
|
|
244
|
+
consensus_confidence: float = 0.0
|
|
245
|
+
disagreements: List[str] = field(default_factory=list)
|
|
246
|
+
started_at: datetime = field(default_factory=datetime.now)
|
|
247
|
+
completed_at: Optional[datetime] = None
|
|
248
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class AgentSelector:
|
|
252
|
+
"""Selects which agents should participate based on task type"""
|
|
253
|
+
|
|
254
|
+
# Task type to agent role mapping
|
|
255
|
+
TASK_AGENT_MAP: Dict[str, List[AgentRole]] = {
|
|
256
|
+
"code_generation": [AgentRole.IMPLEMENTER, AgentRole.ARCHITECT],
|
|
257
|
+
"code_modification": [AgentRole.IMPLEMENTER, AgentRole.REVIEWER],
|
|
258
|
+
"debugging": [AgentRole.DEBUGGER, AgentRole.IMPLEMENTER],
|
|
259
|
+
"code_review": [AgentRole.REVIEWER, AgentRole.SECURITY],
|
|
260
|
+
"security": [AgentRole.SECURITY, AgentRole.REVIEWER],
|
|
261
|
+
"performance": [AgentRole.OPTIMIZER, AgentRole.ARCHITECT],
|
|
262
|
+
"refactoring": [AgentRole.ARCHITECT, AgentRole.REVIEWER, AgentRole.IMPLEMENTER],
|
|
263
|
+
"testing": [AgentRole.TESTER, AgentRole.REVIEWER],
|
|
264
|
+
"documentation": [AgentRole.DOCUMENTATION, AgentRole.IMPLEMENTER],
|
|
265
|
+
"devops": [AgentRole.DEVOPS, AgentRole.SECURITY],
|
|
266
|
+
"explanation": [AgentRole.DOCUMENTATION, AgentRole.ARCHITECT],
|
|
267
|
+
"reasoning": [AgentRole.ARCHITECT, AgentRole.REVIEWER],
|
|
268
|
+
"project_setup": [AgentRole.ARCHITECT, AgentRole.DEVOPS],
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Minimum and maximum agents per session
|
|
272
|
+
MIN_AGENTS = 2
|
|
273
|
+
MAX_AGENTS = 4
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def select_agents(
|
|
277
|
+
cls,
|
|
278
|
+
task_category: str,
|
|
279
|
+
complexity: float,
|
|
280
|
+
explicit_roles: Optional[List[str]] = None
|
|
281
|
+
) -> List[AgentRole]:
|
|
282
|
+
"""
|
|
283
|
+
Select agents for a task based on category and complexity
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
task_category: Type of task (from TaskCategory)
|
|
287
|
+
complexity: Task complexity score (0.0 to 1.0)
|
|
288
|
+
explicit_roles: Optional list of specific roles to include
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
List of AgentRole to consult
|
|
292
|
+
"""
|
|
293
|
+
selected: Set[AgentRole] = set()
|
|
294
|
+
|
|
295
|
+
# Add explicitly requested roles
|
|
296
|
+
if explicit_roles:
|
|
297
|
+
for role_name in explicit_roles:
|
|
298
|
+
try:
|
|
299
|
+
role = AgentRole(role_name.lower())
|
|
300
|
+
selected.add(role)
|
|
301
|
+
except ValueError:
|
|
302
|
+
logger.warning(f"Unknown agent role: {role_name}")
|
|
303
|
+
|
|
304
|
+
# Add roles based on task type
|
|
305
|
+
task_key = task_category.lower().replace("_", "_")
|
|
306
|
+
if task_key in cls.TASK_AGENT_MAP:
|
|
307
|
+
for role in cls.TASK_AGENT_MAP[task_key]:
|
|
308
|
+
selected.add(role)
|
|
309
|
+
|
|
310
|
+
# For high complexity, add more perspectives
|
|
311
|
+
if complexity > 0.8 and len(selected) < cls.MAX_AGENTS:
|
|
312
|
+
# Add architect for complex design decisions
|
|
313
|
+
selected.add(AgentRole.ARCHITECT)
|
|
314
|
+
# Add reviewer for quality assurance
|
|
315
|
+
selected.add(AgentRole.REVIEWER)
|
|
316
|
+
|
|
317
|
+
# For security-sensitive tasks, always include security
|
|
318
|
+
security_keywords = ["auth", "login", "password", "token", "secret", "credential", "encrypt"]
|
|
319
|
+
if any(kw in task_category.lower() for kw in security_keywords):
|
|
320
|
+
selected.add(AgentRole.SECURITY)
|
|
321
|
+
|
|
322
|
+
# Ensure minimum agents
|
|
323
|
+
if len(selected) < cls.MIN_AGENTS:
|
|
324
|
+
# Default to implementer and reviewer
|
|
325
|
+
selected.add(AgentRole.IMPLEMENTER)
|
|
326
|
+
selected.add(AgentRole.REVIEWER)
|
|
327
|
+
|
|
328
|
+
# Limit to max agents
|
|
329
|
+
result = list(selected)[:cls.MAX_AGENTS]
|
|
330
|
+
|
|
331
|
+
return result
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class ConsensusBuilder:
|
|
335
|
+
"""Builds consensus from multiple agent responses"""
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def build_consensus(
|
|
339
|
+
responses: Dict[AgentRole, AgentResponse],
|
|
340
|
+
agent_personas: Dict[AgentRole, AgentPersona]
|
|
341
|
+
) -> tuple[str, float, List[str]]:
|
|
342
|
+
"""
|
|
343
|
+
Build consensus from multiple agent responses
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
responses: Dict of agent role to response
|
|
347
|
+
agent_personas: Dict of agent role to persona (for weights)
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Tuple of (consensus_text, confidence, disagreements)
|
|
351
|
+
"""
|
|
352
|
+
if not responses:
|
|
353
|
+
return "", 0.0, []
|
|
354
|
+
|
|
355
|
+
# Collect all key points and suggestions with weights
|
|
356
|
+
weighted_points: Dict[str, float] = {}
|
|
357
|
+
weighted_suggestions: Dict[str, float] = {}
|
|
358
|
+
all_concerns: List[str] = []
|
|
359
|
+
all_code_snippets: List[str] = []
|
|
360
|
+
|
|
361
|
+
total_weight = 0.0
|
|
362
|
+
total_confidence = 0.0
|
|
363
|
+
|
|
364
|
+
for role, response in responses.items():
|
|
365
|
+
persona = agent_personas.get(role)
|
|
366
|
+
weight = persona.weight if persona else 1.0
|
|
367
|
+
total_weight += weight
|
|
368
|
+
total_confidence += response.confidence * weight
|
|
369
|
+
|
|
370
|
+
# Weight key points
|
|
371
|
+
for point in response.key_points:
|
|
372
|
+
point_lower = point.lower().strip()
|
|
373
|
+
if point_lower not in weighted_points:
|
|
374
|
+
weighted_points[point_lower] = 0.0
|
|
375
|
+
weighted_points[point_lower] += weight * response.confidence
|
|
376
|
+
|
|
377
|
+
# Weight suggestions
|
|
378
|
+
for suggestion in response.suggestions:
|
|
379
|
+
sug_lower = suggestion.lower().strip()
|
|
380
|
+
if sug_lower not in weighted_suggestions:
|
|
381
|
+
weighted_suggestions[sug_lower] = 0.0
|
|
382
|
+
weighted_suggestions[sug_lower] += weight * response.confidence
|
|
383
|
+
|
|
384
|
+
# Collect concerns
|
|
385
|
+
all_concerns.extend(response.concerns)
|
|
386
|
+
|
|
387
|
+
# Collect code snippets (prefer from implementer)
|
|
388
|
+
if role == AgentRole.IMPLEMENTER:
|
|
389
|
+
all_code_snippets = response.code_snippets + all_code_snippets
|
|
390
|
+
else:
|
|
391
|
+
all_code_snippets.extend(response.code_snippets)
|
|
392
|
+
|
|
393
|
+
# Calculate overall confidence
|
|
394
|
+
consensus_confidence = total_confidence / total_weight if total_weight > 0 else 0.0
|
|
395
|
+
|
|
396
|
+
# Find disagreements (points mentioned by some but not others)
|
|
397
|
+
disagreements = []
|
|
398
|
+
for point, weight in weighted_points.items():
|
|
399
|
+
# If a point has low weight relative to total, it's a potential disagreement
|
|
400
|
+
agreement_ratio = weight / total_weight
|
|
401
|
+
if 0.2 < agreement_ratio < 0.6: # Partial agreement
|
|
402
|
+
disagreements.append(f"Partial agreement on: {point}")
|
|
403
|
+
|
|
404
|
+
# Build consensus text
|
|
405
|
+
consensus_parts = []
|
|
406
|
+
|
|
407
|
+
# Add top key points
|
|
408
|
+
sorted_points = sorted(weighted_points.items(), key=lambda x: x[1], reverse=True)
|
|
409
|
+
if sorted_points:
|
|
410
|
+
consensus_parts.append("**Key Points:**")
|
|
411
|
+
for point, _ in sorted_points[:5]:
|
|
412
|
+
consensus_parts.append(f"- {point}")
|
|
413
|
+
|
|
414
|
+
# Add top suggestions
|
|
415
|
+
sorted_suggestions = sorted(weighted_suggestions.items(), key=lambda x: x[1], reverse=True)
|
|
416
|
+
if sorted_suggestions:
|
|
417
|
+
consensus_parts.append("\n**Recommendations:**")
|
|
418
|
+
for suggestion, _ in sorted_suggestions[:5]:
|
|
419
|
+
consensus_parts.append(f"- {suggestion}")
|
|
420
|
+
|
|
421
|
+
# Add concerns if any
|
|
422
|
+
unique_concerns = list(set(all_concerns))
|
|
423
|
+
if unique_concerns:
|
|
424
|
+
consensus_parts.append("\n**Concerns Raised:**")
|
|
425
|
+
for concern in unique_concerns[:3]:
|
|
426
|
+
consensus_parts.append(f"- {concern}")
|
|
427
|
+
|
|
428
|
+
# Add code snippets if any
|
|
429
|
+
if all_code_snippets:
|
|
430
|
+
consensus_parts.append("\n**Code:**")
|
|
431
|
+
for snippet in all_code_snippets[:2]: # Limit code snippets
|
|
432
|
+
consensus_parts.append(f"```\n{snippet}\n```")
|
|
433
|
+
|
|
434
|
+
consensus_text = "\n".join(consensus_parts)
|
|
435
|
+
|
|
436
|
+
return consensus_text, consensus_confidence, disagreements
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class MultiAgentCouncil:
|
|
440
|
+
"""
|
|
441
|
+
Layer 3: Multi-Agent Council
|
|
442
|
+
|
|
443
|
+
Orchestrates multiple AI agents to collaborate on complex tasks.
|
|
444
|
+
Each agent brings a specialized perspective, and the council
|
|
445
|
+
produces a consensus response.
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
def __init__(
|
|
449
|
+
self,
|
|
450
|
+
llm_adapter: Optional[Any] = None,
|
|
451
|
+
agent_personas: Optional[Dict[AgentRole, AgentPersona]] = None,
|
|
452
|
+
max_parallel_agents: int = 3,
|
|
453
|
+
council_threshold: float = 0.75
|
|
454
|
+
):
|
|
455
|
+
"""
|
|
456
|
+
Initialize the council
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
llm_adapter: LLM adapter for agent completions
|
|
460
|
+
agent_personas: Custom agent personas (defaults used if not provided)
|
|
461
|
+
max_parallel_agents: Maximum agents to run in parallel
|
|
462
|
+
council_threshold: Complexity threshold to convene full council
|
|
463
|
+
"""
|
|
464
|
+
self._llm_adapter = llm_adapter
|
|
465
|
+
self.agent_personas = agent_personas or DEFAULT_AGENTS
|
|
466
|
+
self.max_parallel_agents = max_parallel_agents
|
|
467
|
+
self.council_threshold = council_threshold
|
|
468
|
+
self._executor = ThreadPoolExecutor(max_workers=max_parallel_agents)
|
|
469
|
+
self._session_history: List[CouncilSession] = []
|
|
470
|
+
self._lock = threading.Lock()
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def llm_adapter(self):
|
|
474
|
+
"""Lazy load LLM adapter"""
|
|
475
|
+
if self._llm_adapter is None:
|
|
476
|
+
try:
|
|
477
|
+
from ..llm_adapter import get_llm_adapter
|
|
478
|
+
self._llm_adapter = get_llm_adapter()
|
|
479
|
+
except Exception as e:
|
|
480
|
+
logger.warning(f"Could not load LLM adapter: {e}")
|
|
481
|
+
return self._llm_adapter
|
|
482
|
+
|
|
483
|
+
def _generate_session_id(self) -> str:
|
|
484
|
+
"""Generate unique session ID"""
|
|
485
|
+
import uuid
|
|
486
|
+
return f"council_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
|
|
487
|
+
|
|
488
|
+
def _create_agent_prompt(
|
|
489
|
+
self,
|
|
490
|
+
persona: AgentPersona,
|
|
491
|
+
task_description: str,
|
|
492
|
+
context: Optional[Dict[str, Any]] = None
|
|
493
|
+
) -> str:
|
|
494
|
+
"""Create the full prompt for an agent"""
|
|
495
|
+
parts = []
|
|
496
|
+
|
|
497
|
+
# Add context if available
|
|
498
|
+
if context:
|
|
499
|
+
if context.get("code"):
|
|
500
|
+
parts.append(f"**Code to analyze:**\n```\n{context['code']}\n```\n")
|
|
501
|
+
if context.get("error"):
|
|
502
|
+
parts.append(f"**Error:**\n{context['error']}\n")
|
|
503
|
+
if context.get("files"):
|
|
504
|
+
parts.append(f"**Relevant files:** {', '.join(context['files'])}\n")
|
|
505
|
+
|
|
506
|
+
parts.append(f"**Task:**\n{task_description}")
|
|
507
|
+
|
|
508
|
+
parts.append("""
|
|
509
|
+
Please provide your analysis as a structured response:
|
|
510
|
+
1. KEY POINTS: Main observations or findings (bullet points)
|
|
511
|
+
2. SUGGESTIONS: Recommended actions or improvements
|
|
512
|
+
3. CONCERNS: Any issues or risks identified
|
|
513
|
+
4. CODE: Any code snippets (if applicable)
|
|
514
|
+
|
|
515
|
+
Be concise but thorough. Focus on your area of expertise.""")
|
|
516
|
+
|
|
517
|
+
return "\n\n".join(parts)
|
|
518
|
+
|
|
519
|
+
def _parse_agent_response(self, raw_response: str, role: AgentRole) -> AgentResponse:
|
|
520
|
+
"""Parse raw LLM response into structured AgentResponse"""
|
|
521
|
+
key_points = []
|
|
522
|
+
suggestions = []
|
|
523
|
+
concerns = []
|
|
524
|
+
code_snippets = []
|
|
525
|
+
|
|
526
|
+
# Simple parsing of structured response
|
|
527
|
+
current_section = None
|
|
528
|
+
current_content = []
|
|
529
|
+
|
|
530
|
+
for line in raw_response.split('\n'):
|
|
531
|
+
line_lower = line.lower().strip()
|
|
532
|
+
|
|
533
|
+
if 'key point' in line_lower or line_lower.startswith('1.'):
|
|
534
|
+
if current_section and current_content:
|
|
535
|
+
self._add_to_section(current_section, current_content, key_points, suggestions, concerns)
|
|
536
|
+
current_section = 'key_points'
|
|
537
|
+
current_content = []
|
|
538
|
+
elif 'suggestion' in line_lower or 'recommend' in line_lower or line_lower.startswith('2.'):
|
|
539
|
+
if current_section and current_content:
|
|
540
|
+
self._add_to_section(current_section, current_content, key_points, suggestions, concerns)
|
|
541
|
+
current_section = 'suggestions'
|
|
542
|
+
current_content = []
|
|
543
|
+
elif 'concern' in line_lower or 'issue' in line_lower or 'risk' in line_lower or line_lower.startswith('3.'):
|
|
544
|
+
if current_section and current_content:
|
|
545
|
+
self._add_to_section(current_section, current_content, key_points, suggestions, concerns)
|
|
546
|
+
current_section = 'concerns'
|
|
547
|
+
current_content = []
|
|
548
|
+
elif line.startswith('```'):
|
|
549
|
+
# Handle code block
|
|
550
|
+
if current_section == 'code':
|
|
551
|
+
code_snippets.append('\n'.join(current_content))
|
|
552
|
+
current_section = None
|
|
553
|
+
current_content = []
|
|
554
|
+
else:
|
|
555
|
+
current_section = 'code'
|
|
556
|
+
current_content = []
|
|
557
|
+
elif line.strip().startswith('-') or line.strip().startswith('•'):
|
|
558
|
+
# Bullet point
|
|
559
|
+
content = line.strip().lstrip('-•').strip()
|
|
560
|
+
if content:
|
|
561
|
+
current_content.append(content)
|
|
562
|
+
elif line.strip():
|
|
563
|
+
current_content.append(line.strip())
|
|
564
|
+
|
|
565
|
+
# Handle last section
|
|
566
|
+
if current_section and current_content:
|
|
567
|
+
if current_section == 'code':
|
|
568
|
+
code_snippets.append('\n'.join(current_content))
|
|
569
|
+
else:
|
|
570
|
+
self._add_to_section(current_section, current_content, key_points, suggestions, concerns)
|
|
571
|
+
|
|
572
|
+
# If no structure found, use raw response as key points
|
|
573
|
+
if not key_points and not suggestions and not concerns:
|
|
574
|
+
key_points = [raw_response[:500]] # Truncate if too long
|
|
575
|
+
|
|
576
|
+
# Calculate confidence based on response quality
|
|
577
|
+
confidence = 0.7 # Base confidence
|
|
578
|
+
if key_points:
|
|
579
|
+
confidence += 0.1
|
|
580
|
+
if suggestions:
|
|
581
|
+
confidence += 0.1
|
|
582
|
+
if code_snippets:
|
|
583
|
+
confidence += 0.1
|
|
584
|
+
|
|
585
|
+
return AgentResponse(
|
|
586
|
+
agent_role=role,
|
|
587
|
+
content=raw_response,
|
|
588
|
+
confidence=min(1.0, confidence),
|
|
589
|
+
key_points=key_points[:5],
|
|
590
|
+
suggestions=suggestions[:5],
|
|
591
|
+
concerns=concerns[:3],
|
|
592
|
+
code_snippets=code_snippets[:2],
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
def _add_to_section(
|
|
596
|
+
self,
|
|
597
|
+
section: str,
|
|
598
|
+
content: List[str],
|
|
599
|
+
key_points: List[str],
|
|
600
|
+
suggestions: List[str],
|
|
601
|
+
concerns: List[str]
|
|
602
|
+
):
|
|
603
|
+
"""Add content to appropriate section"""
|
|
604
|
+
joined = ' '.join(content)
|
|
605
|
+
if section == 'key_points':
|
|
606
|
+
key_points.extend(content)
|
|
607
|
+
elif section == 'suggestions':
|
|
608
|
+
suggestions.extend(content)
|
|
609
|
+
elif section == 'concerns':
|
|
610
|
+
concerns.extend(content)
|
|
611
|
+
|
|
612
|
+
def _consult_agent(
|
|
613
|
+
self,
|
|
614
|
+
persona: AgentPersona,
|
|
615
|
+
task_description: str,
|
|
616
|
+
context: Optional[Dict[str, Any]] = None
|
|
617
|
+
) -> AgentResponse:
|
|
618
|
+
"""
|
|
619
|
+
Consult a single agent
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
persona: Agent's persona configuration
|
|
623
|
+
task_description: Task to analyze
|
|
624
|
+
context: Additional context
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
AgentResponse with the agent's analysis
|
|
628
|
+
"""
|
|
629
|
+
prompt = self._create_agent_prompt(persona, task_description, context)
|
|
630
|
+
|
|
631
|
+
try:
|
|
632
|
+
if self.llm_adapter:
|
|
633
|
+
response = self.llm_adapter.complete(
|
|
634
|
+
prompt=prompt,
|
|
635
|
+
system_prompt=persona.system_prompt,
|
|
636
|
+
model=persona.preferred_model,
|
|
637
|
+
temperature=persona.temperature,
|
|
638
|
+
max_tokens=persona.max_tokens,
|
|
639
|
+
)
|
|
640
|
+
else:
|
|
641
|
+
# Fallback: return a placeholder response
|
|
642
|
+
response = f"[{persona.name}] Analysis placeholder for: {task_description[:100]}..."
|
|
643
|
+
|
|
644
|
+
return self._parse_agent_response(response, persona.role)
|
|
645
|
+
|
|
646
|
+
except Exception as e:
|
|
647
|
+
logger.error(f"Error consulting {persona.name}: {e}")
|
|
648
|
+
return AgentResponse(
|
|
649
|
+
agent_role=persona.role,
|
|
650
|
+
content=f"Error: {e}",
|
|
651
|
+
confidence=0.0,
|
|
652
|
+
concerns=[f"Agent error: {e}"],
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
def convene(
|
|
656
|
+
self,
|
|
657
|
+
task_description: str,
|
|
658
|
+
task_category: str = "general",
|
|
659
|
+
complexity: float = 0.5,
|
|
660
|
+
context: Optional[Dict[str, Any]] = None,
|
|
661
|
+
agents: Optional[List[str]] = None,
|
|
662
|
+
parallel: bool = True
|
|
663
|
+
) -> CouncilSession:
|
|
664
|
+
"""
|
|
665
|
+
Convene the council for a task
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
task_description: Description of the task
|
|
669
|
+
task_category: Category of the task
|
|
670
|
+
complexity: Task complexity (0.0 to 1.0)
|
|
671
|
+
context: Additional context (code, files, errors)
|
|
672
|
+
agents: Specific agents to consult
|
|
673
|
+
parallel: Run agents in parallel
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
CouncilSession with all responses and consensus
|
|
677
|
+
"""
|
|
678
|
+
# Select agents
|
|
679
|
+
agent_roles = AgentSelector.select_agents(
|
|
680
|
+
task_category=task_category,
|
|
681
|
+
complexity=complexity,
|
|
682
|
+
explicit_roles=agents
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
session = CouncilSession(
|
|
686
|
+
session_id=self._generate_session_id(),
|
|
687
|
+
task_description=task_description,
|
|
688
|
+
agents_consulted=agent_roles,
|
|
689
|
+
metadata={
|
|
690
|
+
"category": task_category,
|
|
691
|
+
"complexity": complexity,
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
logger.info(f"Council convened: {session.session_id} with {[r.value for r in agent_roles]}")
|
|
696
|
+
|
|
697
|
+
# Consult agents
|
|
698
|
+
if parallel and len(agent_roles) > 1:
|
|
699
|
+
# Parallel consultation
|
|
700
|
+
futures = {}
|
|
701
|
+
for role in agent_roles:
|
|
702
|
+
persona = self.agent_personas.get(role)
|
|
703
|
+
if persona:
|
|
704
|
+
future = self._executor.submit(
|
|
705
|
+
self._consult_agent, persona, task_description, context
|
|
706
|
+
)
|
|
707
|
+
futures[future] = role
|
|
708
|
+
|
|
709
|
+
for future in as_completed(futures):
|
|
710
|
+
role = futures[future]
|
|
711
|
+
try:
|
|
712
|
+
response = future.result()
|
|
713
|
+
session.responses[role] = response
|
|
714
|
+
except Exception as e:
|
|
715
|
+
logger.error(f"Error from {role.value}: {e}")
|
|
716
|
+
|
|
717
|
+
else:
|
|
718
|
+
# Sequential consultation
|
|
719
|
+
for role in agent_roles:
|
|
720
|
+
persona = self.agent_personas.get(role)
|
|
721
|
+
if persona:
|
|
722
|
+
response = self._consult_agent(persona, task_description, context)
|
|
723
|
+
session.responses[role] = response
|
|
724
|
+
|
|
725
|
+
# Build consensus
|
|
726
|
+
if session.responses:
|
|
727
|
+
consensus, confidence, disagreements = ConsensusBuilder.build_consensus(
|
|
728
|
+
session.responses,
|
|
729
|
+
self.agent_personas
|
|
730
|
+
)
|
|
731
|
+
session.consensus = consensus
|
|
732
|
+
session.consensus_confidence = confidence
|
|
733
|
+
session.disagreements = disagreements
|
|
734
|
+
|
|
735
|
+
session.completed_at = datetime.now()
|
|
736
|
+
|
|
737
|
+
# Store session
|
|
738
|
+
with self._lock:
|
|
739
|
+
self._session_history.append(session)
|
|
740
|
+
# Keep last 100 sessions
|
|
741
|
+
if len(self._session_history) > 100:
|
|
742
|
+
self._session_history = self._session_history[-100:]
|
|
743
|
+
|
|
744
|
+
return session
|
|
745
|
+
|
|
746
|
+
async def convene_async(
|
|
747
|
+
self,
|
|
748
|
+
task_description: str,
|
|
749
|
+
task_category: str = "general",
|
|
750
|
+
complexity: float = 0.5,
|
|
751
|
+
context: Optional[Dict[str, Any]] = None,
|
|
752
|
+
agents: Optional[List[str]] = None
|
|
753
|
+
) -> CouncilSession:
|
|
754
|
+
"""Async version of convene"""
|
|
755
|
+
loop = asyncio.get_event_loop()
|
|
756
|
+
return await loop.run_in_executor(
|
|
757
|
+
None,
|
|
758
|
+
lambda: self.convene(
|
|
759
|
+
task_description=task_description,
|
|
760
|
+
task_category=task_category,
|
|
761
|
+
complexity=complexity,
|
|
762
|
+
context=context,
|
|
763
|
+
agents=agents,
|
|
764
|
+
parallel=True
|
|
765
|
+
)
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
def quick_consult(
|
|
769
|
+
self,
|
|
770
|
+
task_description: str,
|
|
771
|
+
role: AgentRole
|
|
772
|
+
) -> AgentResponse:
|
|
773
|
+
"""
|
|
774
|
+
Quick consultation with a single agent
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
task_description: Task to analyze
|
|
778
|
+
role: Specific agent role
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
AgentResponse from the agent
|
|
782
|
+
"""
|
|
783
|
+
persona = self.agent_personas.get(role)
|
|
784
|
+
if not persona:
|
|
785
|
+
raise ValueError(f"Unknown agent role: {role}")
|
|
786
|
+
|
|
787
|
+
return self._consult_agent(persona, task_description)
|
|
788
|
+
|
|
789
|
+
def get_session_history(self, limit: int = 10) -> List[CouncilSession]:
|
|
790
|
+
"""Get recent council sessions"""
|
|
791
|
+
with self._lock:
|
|
792
|
+
return self._session_history[-limit:]
|
|
793
|
+
|
|
794
|
+
def get_available_agents(self) -> List[Dict[str, Any]]:
|
|
795
|
+
"""Get list of available agents with descriptions"""
|
|
796
|
+
return [
|
|
797
|
+
{
|
|
798
|
+
"role": role.value,
|
|
799
|
+
"name": persona.name,
|
|
800
|
+
"description": persona.description,
|
|
801
|
+
"focus_areas": persona.focus_areas,
|
|
802
|
+
}
|
|
803
|
+
for role, persona in self.agent_personas.items()
|
|
804
|
+
]
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
# Convenience functions
|
|
808
|
+
def get_council(llm_adapter: Optional[Any] = None) -> MultiAgentCouncil:
|
|
809
|
+
"""Get or create council instance"""
|
|
810
|
+
return MultiAgentCouncil(llm_adapter=llm_adapter)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def quick_council(
|
|
814
|
+
task: str,
|
|
815
|
+
category: str = "general",
|
|
816
|
+
complexity: float = 0.5
|
|
817
|
+
) -> CouncilSession:
|
|
818
|
+
"""Quickly convene council for a task"""
|
|
819
|
+
council = get_council()
|
|
820
|
+
return council.convene(
|
|
821
|
+
task_description=task,
|
|
822
|
+
task_category=category,
|
|
823
|
+
complexity=complexity
|
|
824
|
+
)
|