Add Consensus Tool for Multi-Model Perspective Gathering (#67)

* WIP
Refactor resolving mode_names, should be done once at MCP call boundary
Pass around model context instead
Consensus tool allows one to get a consensus from multiple models, optionally assigning one a 'for' or 'against' stance to find nuanced responses.

* Deduplication of model resolution, model_context should be available before reaching deeper parts of the code
Improved abstraction when building conversations
Throw programmer errors early

* Guardrails
Support for `model:option` format at MCP boundary so future tools can use additional options if needed instead of handling this only for consensus
Model name now supports an optional ":option" for future use

* Simplified async flow

* Improved model for request to support natural language
Simplified async flow

* Improved model for request to support natural language
Simplified async flow

* Fix consensus tool async/sync patterns to match codebase standards

CRITICAL FIXES:
- Converted _get_consensus_responses from async to sync (matches other tools)
- Converted store_conversation_turn from async to sync (add_turn is synchronous)
- Removed unnecessary asyncio imports and sleep calls
- Fixed ClosedResourceError in MCP protocol during long consensus operations

PATTERN ALIGNMENT:
- Consensus tool now follows same sync patterns as all other tools
- Only execute() and prepare_prompt() are async (base class requirement)
- All internal operations are synchronous like analyze, chat, debug, etc.

TESTING:
- MCP simulation test now passes: consensus_stance 
- Two-model consensus works correctly in ~35 seconds
- Unknown stance handling defaults to neutral with warnings
- All 9 unit tests pass (100% success rate)

The consensus tool async patterns were anomalous in the codebase.
This fix aligns it with the established synchronous patterns used
by all other tools while maintaining full functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fixed call order and added new test

* Cleanup dead comments
Docs for the new tool
Improved tests

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Beehive Innovations
2025-06-17 10:53:17 +04:00
committed by GitHub
parent 9b98df650b
commit 95556ba9ea
31 changed files with 2643 additions and 324 deletions

View File

@@ -196,9 +196,7 @@ def detect_file_type(file_path: str) -> str:
"""
Detect file type for appropriate processing strategy.
NOTE: This function is currently not used for line number auto-detection
due to backward compatibility requirements. It is intended for future
features requiring specific file type handling (e.g., image processing,
This function is intended for specific file type handling (e.g., image processing,
binary file analysis, or enhanced file filtering).
Args:
@@ -247,7 +245,7 @@ def should_add_line_numbers(file_path: str, include_line_numbers: Optional[bool]
if include_line_numbers is not None:
return include_line_numbers
# Default: DO NOT add line numbers (backwards compatibility)
# Default: DO NOT add line numbers
# Tools that want line numbers must explicitly request them
return False
@@ -1026,7 +1024,7 @@ def read_file_safely(file_path: str, max_size: int = 10 * 1024 * 1024) -> Option
return None
def check_total_file_size(files: list[str], model_name: Optional[str] = None) -> Optional[dict]:
def check_total_file_size(files: list[str], model_name: str) -> Optional[dict]:
"""
Check if total file sizes would exceed token threshold before embedding.
@@ -1034,9 +1032,12 @@ def check_total_file_size(files: list[str], model_name: Optional[str] = None) ->
No partial inclusion - either all files fit or request is rejected.
This forces Claude to make better file selection decisions.
This function MUST be called with the effective model name (after resolution).
It should never receive 'auto' or None - model resolution happens earlier.
Args:
files: List of file paths to check
model_name: Model name for context-aware thresholds, or None for default
model_name: The resolved model name for context-aware thresholds (required)
Returns:
Dict with `code_too_large` response if too large, None if acceptable
@@ -1044,17 +1045,14 @@ def check_total_file_size(files: list[str], model_name: Optional[str] = None) ->
if not files:
return None
# Get model-specific token allocation (dynamic thresholds)
if not model_name:
from config import DEFAULT_MODEL
# Validate we have a proper model name (not auto or None)
if not model_name or model_name.lower() == "auto":
raise ValueError(
f"check_total_file_size called with unresolved model: '{model_name}'. "
"Model must be resolved before file size checking."
)
model_name = DEFAULT_MODEL
# Handle auto mode gracefully
if model_name.lower() == "auto":
from providers.registry import ModelProviderRegistry
model_name = ModelProviderRegistry.get_preferred_fallback_model()
logger.info(f"File size check: Using model '{model_name}' for token limit calculation")
from utils.model_context import ModelContext
@@ -1091,6 +1089,7 @@ def check_total_file_size(files: list[str], model_name: Optional[str] = None) ->
"file_count": file_count,
"threshold_percent": threshold_percent,
"model_context_window": context_window,
"model_name": model_name,
"instructions": "Reduce file selection and try again - all files must fit within budget",
},
}