feat: optimize analyze_code for Claude Code with improved formatting

BREAKING CHANGES:
- Remove verbose_output from tool schema (Claude Code can't accidentally use it)
- Always show minimal terminal output with file previews
- Improved file content formatting for Gemini with clear delimiters

Key improvements:
- Files formatted as "--- BEGIN FILE: path --- content --- END FILE: path ---"
- Direct code formatted as "--- BEGIN DIRECT CODE --- code --- END DIRECT CODE ---"
- Terminal shows file paths, sizes, and small previews (not full content)
- Clear prompt structure for Gemini: USER REQUEST | CODE TO ANALYZE sections
- Prevents terminal hangs/glitches with large files in Claude Code
- All tests updated and passing

This ensures Claude Code stays responsive while Gemini gets properly formatted content.

🤖 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:59:35 +04:00
parent c2c80dd828
commit 341022a35b
3 changed files with 131 additions and 68 deletions

View File

@@ -109,25 +109,31 @@ def configure_gemini():
def read_file_content(file_path: str) -> str: def read_file_content(file_path: str) -> str:
"""Read content from a file with error handling""" """Read content from a file with error handling - for backward compatibility"""
return read_file_content_for_gemini(file_path)
def read_file_content_for_gemini(file_path: str) -> str:
"""Read content from a file with proper formatting for Gemini"""
try: try:
path = Path(file_path) path = Path(file_path)
if not path.exists(): if not path.exists():
return f"Error: File not found: {file_path}" return f"\n--- FILE NOT FOUND: {file_path} ---\nError: File does not exist\n--- END FILE ---\n"
if not path.is_file(): if not path.is_file():
return f"Error: Not a file: {file_path}" return f"\n--- NOT A FILE: {file_path} ---\nError: Path is not a file\n--- END FILE ---\n"
# Read the file # Read the file
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
content = f.read() content = f.read()
return f"=== File: {file_path} ===\n{content}\n" # Format with clear delimiters for Gemini
return f"\n--- BEGIN FILE: {file_path} ---\n{content}\n--- END FILE: {file_path} ---\n"
except Exception as e: except Exception as e:
return f"Error reading {file_path}: {str(e)}" return f"\n--- ERROR READING FILE: {file_path} ---\nError: {str(e)}\n--- END FILE ---\n"
def prepare_code_context( def prepare_code_context(
files: Optional[List[str]], code: Optional[str], verbose: bool = False files: Optional[List[str]], code: Optional[str]
) -> Tuple[str, str]: ) -> 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) Returns: (context_for_gemini, summary_for_terminal)
@@ -137,26 +143,45 @@ def prepare_code_context(
# Add file contents # Add file contents
if files: if files:
summary_parts.append(f"Analyzing {len(files)} file(s):") summary_parts.append(f"📁 Analyzing {len(files)} file(s):")
for file_path in files: for file_path in files:
content = read_file_content(file_path) # Get file content for Gemini
context_parts.append(content) file_content = read_file_content_for_gemini(file_path)
context_parts.append(file_content)
# For summary, just show file path and size # Create summary with small excerpt for terminal
path = Path(file_path) path = Path(file_path)
if path.exists() and path.is_file(): if path.exists() and path.is_file():
size = path.stat().st_size size = path.stat().st_size
summary_parts.append(f" - {file_path} ({size:,} bytes)") try:
with open(path, "r", encoding="utf-8") as f:
# Read first few lines for preview
preview_lines = []
for i, line in enumerate(f):
if i >= 3: # Show max 3 lines
break
preview_lines.append(line.rstrip())
preview = "\n".join(preview_lines)
if len(preview) > 100:
preview = preview[:100] + "..."
summary_parts.append(f" 📄 {file_path} ({size:,} bytes)")
if preview.strip():
summary_parts.append(f" Preview: {preview[:50]}...")
except Exception:
summary_parts.append(f" 📄 {file_path} ({size:,} bytes)")
else: else:
summary_parts.append(f" - {file_path} (not found)") 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") formatted_code = f"\n--- BEGIN DIRECT CODE ---\n{code}\n--- END DIRECT CODE ---\n"
summary_parts.append(f"Direct code provided ({len(code):,} characters)") context_parts.append(formatted_code)
preview = code[:100] + "..." if len(code) > 100 else code
summary_parts.append(f"💻 Direct code provided ({len(code):,} characters)")
summary_parts.append(f" Preview: {preview}")
full_context = "\n".join(context_parts) full_context = "\n\n".join(context_parts)
summary = "\n".join(summary_parts) if not verbose else full_context summary = "\n".join(summary_parts)
return full_context, summary return full_context, summary
@@ -241,11 +266,6 @@ async def handle_list_tools() -> List[Tool]:
"description": f"Model to use (defaults to {DEFAULT_MODEL})", "description": f"Model to use (defaults to {DEFAULT_MODEL})",
"default": DEFAULT_MODEL, "default": DEFAULT_MODEL,
}, },
"verbose_output": {
"type": "boolean",
"description": "Show file contents in terminal output",
"default": False,
},
}, },
"required": ["question"], "required": ["question"],
}, },
@@ -320,9 +340,9 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextCon
] ]
try: try:
# Prepare code context # Prepare code context - always use non-verbose mode for Claude Code compatibility
code_context, summary = prepare_code_context( code_context, summary = prepare_code_context(
request.files, request.code, request.verbose_output request.files, request.code
) )
# Count approximate tokens (rough estimate: 1 token ≈ 4 characters) # Count approximate tokens (rough estimate: 1 token ≈ 4 characters)
@@ -346,12 +366,19 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextCon
}, },
) )
# Prepare the full prompt with enhanced developer context # Prepare the full prompt with enhanced developer context and clear structure
system_prompt = request.system_prompt or DEVELOPER_SYSTEM_PROMPT system_prompt = request.system_prompt or DEVELOPER_SYSTEM_PROMPT
full_prompt = ( full_prompt = f"""{system_prompt}
f"{system_prompt}\n\nCode to analyze:\n\n{code_context}\n\n"
f"Question/Request: {request.question}" === USER REQUEST ===
) {request.question}
=== END USER REQUEST ===
=== CODE TO ANALYZE ===
{code_context}
=== END CODE TO ANALYZE ===
Please analyze the code above and respond to the user's request. The code files are clearly marked with their paths and content boundaries."""
# Generate response # Generate response
response = model.generate_content(full_prompt) response = model.generate_content(full_prompt)
@@ -367,9 +394,9 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextCon
) )
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 # Always return response with summary for Claude Code compatibility
if not request.verbose_output and request.files: if request.files or request.code:
response_text = f"{summary}\n\nGemini's response:\n{text}" response_text = f"{summary}\n\n🤖 Gemini's Analysis:\n{text}"
else: else:
response_text = text response_text = text

View File

@@ -61,7 +61,6 @@ 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:
@@ -73,7 +72,8 @@ class TestFileOperations:
test_file.write_text("def hello():\n return 'world'", encoding='utf-8') test_file.write_text("def hello():\n return 'world'", encoding='utf-8')
content = read_file_content(str(test_file)) content = read_file_content(str(test_file))
assert "=== File:" in content assert "--- BEGIN FILE:" in content
assert "--- END FILE:" in content
assert "def hello():" in content assert "def hello():" in content
assert "return 'world'" in content assert "return 'world'" in content
@@ -84,12 +84,14 @@ class TestFileOperations:
os.path.sep, "nonexistent_dir_12345", "nonexistent_file.py" os.path.sep, "nonexistent_dir_12345", "nonexistent_file.py"
) )
content = read_file_content(nonexistent_path) content = read_file_content(nonexistent_path)
assert "Error: File not found" in content assert "--- FILE NOT FOUND:" in content
assert "Error: File does not exist" in content
def test_read_file_content_directory(self, tmp_path): def test_read_file_content_directory(self, tmp_path):
"""Test reading a directory instead of file""" """Test reading a directory instead of file"""
content = read_file_content(str(tmp_path)) content = read_file_content(str(tmp_path))
assert "Error: Not a file" in content assert "--- NOT A FILE:" in content
assert "Error: Path is not a file" in content
def test_prepare_code_context_with_files(self, tmp_path): def test_prepare_code_context_with_files(self, tmp_path):
"""Test preparing context from files""" """Test preparing context from files"""
@@ -99,20 +101,23 @@ class TestFileOperations:
file2.write_text("print('file2')", encoding='utf-8') file2.write_text("print('file2')", encoding='utf-8')
context, summary = prepare_code_context([str(file1), str(file2)], None) context, summary = prepare_code_context([str(file1), str(file2)], None)
assert "--- BEGIN FILE:" in context
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 "--- END FILE:" in context
assert "📁 Analyzing 2 file(s)" in summary
assert "bytes)" 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, summary = prepare_code_context(None, code) context, summary = prepare_code_context(None, code)
assert "=== Direct Code ===" in context assert "--- BEGIN DIRECT CODE ---" in context
assert "--- END DIRECT CODE ---" in context
assert code in context assert code in context
assert "Direct code provided" in summary 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"""
@@ -123,8 +128,8 @@ class TestFileOperations:
context, summary = 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 "📁 Analyzing 1 file(s)" in summary
assert "Direct code provided" in summary assert "💻 Direct code provided" in summary
class TestToolHandlers: class TestToolHandlers:
@@ -227,8 +232,8 @@ class TestToolHandlers:
assert len(result) == 1 assert len(result) == 1
# Check that the response contains both summary and Gemini's response # Check that the response contains both summary and Gemini's response
response_text = result[0].text response_text = result[0].text
assert "Analyzing 1 file(s)" in response_text assert "📁 Analyzing 1 file(s)" in response_text
assert "Gemini's response:" in response_text assert "🤖 Gemini's Analysis:" in response_text
assert "Analysis result" in response_text assert "Analysis result" in response_text
@pytest.mark.asyncio @pytest.mark.asyncio

View File

@@ -14,36 +14,43 @@ if str(parent_dir) not in sys.path:
from gemini_server import prepare_code_context from gemini_server import prepare_code_context
class TestVerboseOutput: class TestNewFormattingBehavior:
"""Test verbose output functionality""" """Test the improved formatting behavior"""
def test_verbose_true_shows_full_content(self, tmp_path): def test_file_formatting_for_gemini(self, tmp_path):
"""Test that verbose=True shows full file content""" """Test that files are properly formatted for Gemini"""
test_file = tmp_path / "test.py" test_file = tmp_path / "test.py"
content = "def hello():\n return 'world'" content = "def hello():\n return 'world'"
test_file.write_text(content, encoding='utf-8') test_file.write_text(content, encoding='utf-8')
context, summary = prepare_code_context([str(test_file)], None, verbose=True) context, summary = prepare_code_context([str(test_file)], None)
# With verbose=True, summary should equal context # Context should have clear markers for Gemini
assert summary == context assert "--- BEGIN FILE:" in context
assert content in summary assert "--- END FILE:" in context
assert str(test_file) in context
assert content in context
def test_verbose_false_shows_summary(self, tmp_path): # Summary should be concise for terminal
"""Test that verbose=False shows only summary""" assert "📁 Analyzing 1 file(s)" in summary
assert "bytes)" in summary
assert len(summary) < len(context) # Summary much smaller than full context
def test_terminal_summary_shows_preview(self, tmp_path):
"""Test that terminal summary shows small preview"""
test_file = tmp_path / "large_file.py" test_file = tmp_path / "large_file.py"
content = "x = 1\n" * 1000 # Large content content = "# This is a large file\n" + "x = 1\n" * 1000
test_file.write_text(content, encoding='utf-8') test_file.write_text(content, encoding='utf-8')
context, summary = prepare_code_context([str(test_file)], None, verbose=False) context, summary = prepare_code_context([str(test_file)], None)
# Summary should be much smaller than context # Summary should show preview but not full content
assert len(summary) < len(context) assert "📁 Analyzing 1 file(s)" in summary
assert "Analyzing 1 file(s)" in summary
assert str(test_file) in summary assert str(test_file) in summary
assert "bytes)" in summary assert "bytes)" in summary
# Content should not be in summary assert "Preview:" in summary
assert content not in summary # Full content should not be in summary
assert "x = 1" not in summary or summary.count("x = 1") < 5
def test_multiple_files_summary(self, tmp_path): def test_multiple_files_summary(self, tmp_path):
"""Test summary with multiple files""" """Test summary with multiple files"""
@@ -53,22 +60,46 @@ class TestVerboseOutput:
file.write_text(f"# File {i}\nprint({i})", encoding='utf-8') file.write_text(f"# File {i}\nprint({i})", encoding='utf-8')
files.append(str(file)) files.append(str(file))
context, summary = prepare_code_context(files, None, verbose=False) context, summary = prepare_code_context(files, None)
assert "Analyzing 3 file(s)" in summary assert "📁 Analyzing 3 file(s)" in summary
for file in files: for file in files:
assert file in summary assert file in summary
assert "bytes)" in summary assert "bytes)" in summary
# Should have clear delimiters in context
assert context.count("--- BEGIN FILE:") == 3
assert context.count("--- END FILE:") == 3
def test_code_and_files_summary(self, tmp_path): def test_direct_code_formatting(self):
"""Test summary with both files and direct code""" """Test direct code formatting"""
direct_code = "# Direct code\nprint('hello')"
context, summary = prepare_code_context(None, direct_code)
# Context should have clear markers
assert "--- BEGIN DIRECT CODE ---" in context
assert "--- END DIRECT CODE ---" in context
assert direct_code in context
# Summary should show preview
assert "💻 Direct code provided" in summary
assert f"({len(direct_code)} characters)" in summary
assert "Preview:" in summary
def test_mixed_content_formatting(self, tmp_path):
"""Test formatting with both files and direct code"""
test_file = tmp_path / "test.py" test_file = tmp_path / "test.py"
test_file.write_text("# Test file", encoding='utf-8') test_file.write_text("# Test file", encoding='utf-8')
direct_code = "# Direct code\nprint('hello')" direct_code = "# Direct code\nprint('hello')"
context, summary = prepare_code_context([str(test_file)], direct_code, verbose=False) context, summary = prepare_code_context([str(test_file)], direct_code)
assert "Analyzing 1 file(s)" in summary # Context should have both with clear separation
assert str(test_file) in summary assert "--- BEGIN FILE:" in context
assert "Direct code provided" in summary assert "--- END FILE:" in context
assert f"({len(direct_code):,} characters)" in summary assert "--- BEGIN DIRECT CODE ---" in context
assert "--- END DIRECT CODE ---" in context
# Summary should mention both
assert "📁 Analyzing 1 file(s)" in summary
assert "💻 Direct code provided" in summary