feat: improve analyze_code terminal output with non-verbose mode

- Add verbose_output parameter (default: False) to CodeAnalysisRequest
- Modify prepare_code_context to return both full context and summary
- Show only file paths and sizes in terminal by default, not full content
- Full file content is still sent to Gemini for analysis
- Add comprehensive tests for verbose output functionality

This prevents terminal hangs when analyzing large files while still providing
Gemini with complete file contents for analysis.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Fahad
2025-06-08 20:49:01 +04:00
parent 0c94de4ac0
commit 22d387a858
3 changed files with 118 additions and 10 deletions

View File

@@ -64,6 +64,7 @@ class CodeAnalysisRequest(BaseModel):
max_tokens: Optional[int] = Field(8192, description="Maximum number of tokens in response") max_tokens: Optional[int] = Field(8192, description="Maximum number of tokens in response")
temperature: Optional[float] = Field(0.2, description="Temperature for code analysis (0-1, default 0.2 for high accuracy)") temperature: Optional[float] = Field(0.2, description="Temperature for code analysis (0-1, default 0.2 for high accuracy)")
model: Optional[str] = Field(DEFAULT_MODEL, description=f"Model to use (defaults to {DEFAULT_MODEL})") model: Optional[str] = Field(DEFAULT_MODEL, description=f"Model to use (defaults to {DEFAULT_MODEL})")
verbose_output: Optional[bool] = Field(False, description="Show file contents in terminal output")
# Create the MCP server instance # Create the MCP server instance
@@ -97,20 +98,37 @@ def read_file_content(file_path: str) -> str:
return f"Error reading {file_path}: {str(e)}" return f"Error reading {file_path}: {str(e)}"
def prepare_code_context(files: Optional[List[str]], code: Optional[str]) -> str: def prepare_code_context(files: Optional[List[str]], code: Optional[str], verbose: bool = False) -> tuple[str, str]:
"""Prepare code context from files and/or direct code""" """Prepare code context from files and/or direct code
Returns: (context_for_gemini, summary_for_terminal)
"""
context_parts = [] context_parts = []
summary_parts = []
# Add file contents # Add file contents
if files: if files:
summary_parts.append(f"Analyzing {len(files)} file(s):")
for file_path in files: for file_path in files:
context_parts.append(read_file_content(file_path)) content = read_file_content(file_path)
context_parts.append(content)
# For summary, just show file path and size
path = Path(file_path)
if path.exists() and path.is_file():
size = path.stat().st_size
summary_parts.append(f" - {file_path} ({size:,} bytes)")
else:
summary_parts.append(f" - {file_path} (not found)")
# Add direct code # Add direct code
if code: if code:
context_parts.append("=== Direct Code ===\n" + code + "\n") context_parts.append("=== Direct Code ===\n" + code + "\n")
summary_parts.append(f"Direct code provided ({len(code):,} characters)")
return "\n".join(context_parts) full_context = "\n".join(context_parts)
summary = "\n".join(summary_parts) if not verbose else full_context
return full_context, summary
@server.list_tools() @server.list_tools()
@@ -268,7 +286,7 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextCon
try: try:
# Prepare code context # Prepare code context
code_context = prepare_code_context(request.files, request.code) code_context, summary = prepare_code_context(request.files, request.code, request.verbose_output)
# Count approximate tokens (rough estimate: 1 token ≈ 4 characters) # Count approximate tokens (rough estimate: 1 token ≈ 4 characters)
estimated_tokens = len(code_context) // 4 estimated_tokens = len(code_context) // 4
@@ -302,9 +320,15 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextCon
finish_reason = response.candidates[0].finish_reason if response.candidates else "Unknown" finish_reason = response.candidates[0].finish_reason if response.candidates else "Unknown"
text = f"Response blocked or incomplete. Finish reason: {finish_reason}" text = f"Response blocked or incomplete. Finish reason: {finish_reason}"
# Return response with summary if not verbose
if not request.verbose_output and request.files:
response_text = f"{summary}\n\nGemini's response:\n{text}"
else:
response_text = text
return [TextContent( return [TextContent(
type="text", type="text",
text=text text=response_text
)] )]
except Exception as e: except Exception as e:

View File

@@ -61,6 +61,7 @@ class TestModels:
assert request.max_tokens == 8192 assert request.max_tokens == 8192
assert request.temperature == 0.2 assert request.temperature == 0.2
assert request.model == DEFAULT_MODEL assert request.model == DEFAULT_MODEL
assert request.verbose_output == False
class TestFileOperations: class TestFileOperations:
@@ -97,18 +98,21 @@ class TestFileOperations:
file2 = tmp_path / "file2.py" file2 = tmp_path / "file2.py"
file2.write_text("print('file2')", encoding='utf-8') file2.write_text("print('file2')", encoding='utf-8')
context = prepare_code_context([str(file1), str(file2)], None) context, summary = prepare_code_context([str(file1), str(file2)], None)
assert "file1.py" in context assert "file1.py" in context
assert "file2.py" in context assert "file2.py" in context
assert "print('file1')" in context assert "print('file1')" in context
assert "print('file2')" in context assert "print('file2')" in context
assert "Analyzing 2 file(s)" in summary
assert "bytes)" in summary
def test_prepare_code_context_with_code(self): def test_prepare_code_context_with_code(self):
"""Test preparing context from direct code""" """Test preparing context from direct code"""
code = "def test():\n pass" code = "def test():\n pass"
context = prepare_code_context(None, code) context, summary = prepare_code_context(None, code)
assert "=== Direct Code ===" in context assert "=== Direct Code ===" in context
assert code in context assert code in context
assert "Direct code provided" in summary
def test_prepare_code_context_mixed(self, tmp_path): def test_prepare_code_context_mixed(self, tmp_path):
"""Test preparing context from both files and code""" """Test preparing context from both files and code"""
@@ -116,9 +120,11 @@ class TestFileOperations:
test_file.write_text("# From file", encoding='utf-8') test_file.write_text("# From file", encoding='utf-8')
code = "# Direct code" code = "# Direct code"
context = prepare_code_context([str(test_file)], code) context, summary = prepare_code_context([str(test_file)], code)
assert "# From file" in context assert "# From file" in context
assert "# Direct code" in context assert "# Direct code" in context
assert "Analyzing 1 file(s)" in summary
assert "Direct code provided" in summary
class TestToolHandlers: class TestToolHandlers:
@@ -219,7 +225,11 @@ class TestToolHandlers:
}) })
assert len(result) == 1 assert len(result) == 1
assert result[0].text == "Analysis result" # Check that the response contains both summary and Gemini's response
response_text = result[0].text
assert "Analyzing 1 file(s)" in response_text
assert "Gemini's response:" in response_text
assert "Analysis result" in response_text
@pytest.mark.asyncio @pytest.mark.asyncio
@patch('google.generativeai.list_models') @patch('google.generativeai.list_models')

View File

@@ -0,0 +1,74 @@
"""
Test verbose output functionality
"""
import pytest
from pathlib import Path
import sys
# Add parent directory to path for imports
parent_dir = Path(__file__).resolve().parent.parent
if str(parent_dir) not in sys.path:
sys.path.insert(0, str(parent_dir))
from gemini_server import prepare_code_context
class TestVerboseOutput:
"""Test verbose output functionality"""
def test_verbose_true_shows_full_content(self, tmp_path):
"""Test that verbose=True shows full file content"""
test_file = tmp_path / "test.py"
content = "def hello():\n return 'world'"
test_file.write_text(content, encoding='utf-8')
context, summary = prepare_code_context([str(test_file)], None, verbose=True)
# With verbose=True, summary should equal context
assert summary == context
assert content in summary
def test_verbose_false_shows_summary(self, tmp_path):
"""Test that verbose=False shows only summary"""
test_file = tmp_path / "large_file.py"
content = "x = 1\n" * 1000 # Large content
test_file.write_text(content, encoding='utf-8')
context, summary = prepare_code_context([str(test_file)], None, verbose=False)
# Summary should be much smaller than context
assert len(summary) < len(context)
assert "Analyzing 1 file(s)" in summary
assert str(test_file) in summary
assert "bytes)" in summary
# Content should not be in summary
assert content not in summary
def test_multiple_files_summary(self, tmp_path):
"""Test summary with multiple files"""
files = []
for i in range(3):
file = tmp_path / f"file{i}.py"
file.write_text(f"# File {i}\nprint({i})", encoding='utf-8')
files.append(str(file))
context, summary = prepare_code_context(files, None, verbose=False)
assert "Analyzing 3 file(s)" in summary
for file in files:
assert file in summary
assert "bytes)" in summary
def test_code_and_files_summary(self, tmp_path):
"""Test summary with both files and direct code"""
test_file = tmp_path / "test.py"
test_file.write_text("# Test file", encoding='utf-8')
direct_code = "# Direct code\nprint('hello')"
context, summary = prepare_code_context([str(test_file)], direct_code, verbose=False)
assert "Analyzing 1 file(s)" in summary
assert str(test_file) in summary
assert "Direct code provided" in summary
assert f"({len(direct_code):,} characters)" in summary