diff --git a/src/drd/cli/query/dynamic_command_handler.py b/src/drd/cli/query/dynamic_command_handler.py index 6154a89..80144ea 100644 --- a/src/drd/cli/query/dynamic_command_handler.py +++ b/src/drd/cli/query/dynamic_command_handler.py @@ -1,6 +1,7 @@ import traceback import click from ...api.main import call_dravid_api +import xml.etree.ElementTree as ET from ...utils import print_error, print_success, print_info, print_step, print_debug from ...metadata.common_utils import generate_file_description from ...prompts.error_resolution_prompt import get_error_resolution_prompt @@ -75,6 +76,8 @@ def handle_file_operation(cmd, executor, metadata_manager): elif operation_performed: print_success( f"Successfully performed {cmd['operation']} on file: {cmd['filename']}") + if cmd['operation'] in ['CREATE', 'UPDATE']: + update_file_metadata(cmd, metadata_manager, executor) return "Success" else: raise Exception( @@ -94,21 +97,57 @@ def handle_metadata_operation(cmd, metadata_manager): def update_file_metadata(cmd, metadata_manager, executor): - project_context = metadata_manager.get_project_context() - folder_structure = executor.get_folder_structure() - file_type, description, exports = generate_file_description( - cmd['filename'], - cmd.get('content', ''), - project_context, - folder_structure - ) - metadata_manager.update_file_metadata( - cmd['filename'], - file_type, - cmd.get('content', ''), - description, - exports - ) + file_info = metadata_manager.analyze_file(cmd['filename']) + if file_info: + metadata_manager.update_file_metadata( + file_info['path'], + file_info['type'], + cmd.get('content', ''), + file_info['summary'], + file_info['exports'], + file_info['imports'] + ) + + # Handle dependencies from the XML response + handle_dependencies(file_info, metadata_manager) + + +def handle_dependencies(file_info, metadata_manager): + if 'xml_response' in file_info: + try: + root = ET.fromstring(file_info['xml_response']) + dependencies = root.find('.//external_dependencies') + if dependencies is not None: + for dep in dependencies.findall('dependency'): + dependency_info = dep.text.strip() + metadata_manager.add_external_dependency(dependency_info) + print_info( + f"Added {len(dependencies)} dependencies to the project metadata.") + + # Handle other metadata updates + update_project_info(root, metadata_manager) + update_dev_server_info(root, metadata_manager) + except ET.ParseError: + print_error("Failed to parse XML response for dependencies") + + +def update_project_info(root, metadata_manager): + project_info = root.find('.//project_info') + if project_info is not None: + for field in ['name', 'version', 'description']: + element = project_info.find(field) + if element is not None and element.text: + metadata_manager.metadata['project_info'][field] = element.text.strip( + ) + + +def update_dev_server_info(root, metadata_manager): + dev_server = root.find('.//dev_server') + if dev_server is not None: + start_command = dev_server.find('start_command') + if start_command is not None and start_command.text: + metadata_manager.metadata['dev_server']['start_command'] = start_command.text.strip( + ) def handle_error_with_dravid(error, cmd, executor, metadata_manager, depth=0, previous_context="", debug=False): diff --git a/src/drd/cli/query/main.py b/src/drd/cli/query/main.py index 573e01b..1d3dea0 100644 --- a/src/drd/cli/query/main.py +++ b/src/drd/cli/query/main.py @@ -51,7 +51,6 @@ def execute_dravid_command(query, image_path, debug, instruction_prompt, warn=No full_query = construct_full_query( query, executor, project_context, files_info, reference_files) - print(full_query, "full query") print_info("💡 Preparing to send query to LLM...", indent=2) if image_path: @@ -80,9 +79,7 @@ def execute_dravid_command(query, image_path, debug, instruction_prompt, warn=No success, step_completed, error_message, all_outputs = execute_commands( commands, executor, metadata_manager, debug=debug) - print("no scucess", success) if not success: - print("called") print_error( f"Failed to execute command at step {step_completed}.") print_error(f"Error message: {error_message}") @@ -111,7 +108,6 @@ def execute_dravid_command(query, image_path, debug, instruction_prompt, warn=No def construct_full_query(query, executor, project_context, files_info=None, reference_files=None): is_empty = is_directory_empty(executor.current_dir) - if is_empty: print_info( "Current directory is empty. Will create a new project.", indent=2) @@ -123,41 +119,32 @@ def construct_full_query(query, executor, project_context, files_info=None, refe else: print_info( "Constructing query with project context and file information.", indent=2) - project_guidelines = fetch_project_guidelines(executor.current_dir) - full_query = f"{project_context}\n\n" full_query += f"Project Guidelines:\n{project_guidelines}\n\n" - - if files_info: - if files_info['file_contents_to_load']: + if files_info and isinstance(files_info, dict): + if 'file_contents_to_load' in files_info: file_contents = {} for file in files_info['file_contents_to_load']: content = get_file_content(file) if content: file_contents[file] = content print_info(f" - Read content of {file}", indent=4) - file_context = "\n".join( [f"Current content of {file}:\n{content}" for file, content in file_contents.items()]) full_query += f"Current file contents:\n{file_context}\n\n" - - if files_info['dependencies']: + if 'dependencies' in files_info: dependency_context = "\n".join( [f"Dependency {dep['file']} exports: {', '.join(dep['imports'])}" for dep in files_info['dependencies']]) full_query += f"Dependencies:\n{dependency_context}\n\n" - - if files_info['new_files']: + if 'new_files' in files_info: new_files_context = "\n".join( [f"New file to create: {new_file['file']}" for new_file in files_info['new_files']]) full_query += f"New files to create:\n{new_files_context}\n\n" - - if files_info['main_file']: + if 'main_file' in files_info: full_query += f"Main file to modify: {files_info['main_file']}\n\n" - full_query += "Current directory is not empty.\n\n" full_query += f"User query: {query}" - if reference_files: print_info("📄 Reading reference file contents...", indent=2) reference_contents = {} @@ -166,9 +153,7 @@ def construct_full_query(query, executor, project_context, files_info=None, refe if content: reference_contents[file] = content print_info(f" - Read content of {file}", indent=4) - reference_context = "\n\n".join( [f"Reference file {file}:\n{content}" for file, content in reference_contents.items()]) full_query += f"\n\nReference files:\n{reference_context}" - return full_query diff --git a/src/drd/metadata/project_metadata.py b/src/drd/metadata/project_metadata.py index 9b406e9..19054ab 100644 --- a/src/drd/metadata/project_metadata.py +++ b/src/drd/metadata/project_metadata.py @@ -158,7 +158,7 @@ async def analyze_file(self, file_path): file_info = { "path": rel_path, "type": metadata.find('type').text, - "summary": metadata.find('description').text, + "summary": metadata.find('summary').text, "exports": metadata.find('exports').text.split(',') if metadata.find('exports').text != 'None' else [], "imports": metadata.find('imports').text.split(',') if metadata.find('imports').text != 'None' else [] } @@ -250,3 +250,35 @@ def update_file_metadata(self, filename, file_type, content, description=None, e 'imports': imports or [] }) self.save_metadata() + + def update_metadata_from_file(self): + if os.path.exists(self.metadata_file): + with open(self.metadata_file, 'r') as f: + content = f.read() + try: + new_metadata = json.loads(content) + # Update dev server info if present + if 'dev_server' in new_metadata: + self.metadata['dev_server'] = new_metadata['dev_server'] + # Update other metadata fields + for key, value in new_metadata.items(): + if key != 'files': # We'll handle files separately + self.metadata[key] = value + # Update file metadata + if 'files' in new_metadata: + for file_entry in new_metadata['files']: + filename = file_entry['filename'] + file_type = file_entry.get( + 'type', filename.split('.')[-1]) + file_content = file_entry.get('content', '') + description = file_entry.get('description', '') + exports = file_entry.get('exports', []) + imports = file_entry.get('imports', []) + self.update_file_metadata( + filename, file_type, file_content, description, exports, imports) + self.save_metadata() + return True + except json.JSONDecodeError: + print(f"Error: Invalid JSON content in {self.metadata_file}") + return False + return False diff --git a/src/drd/metadata/rate_limit_handler.py b/src/drd/metadata/rate_limit_handler.py index 06883ab..a83e96e 100644 --- a/src/drd/metadata/rate_limit_handler.py +++ b/src/drd/metadata/rate_limit_handler.py @@ -48,24 +48,26 @@ async def process_single_file(filename, content, project_context, folder_structu async with rate_limiter.semaphore: await rate_limiter.acquire() response = await to_thread(call_dravid_api_with_pagination, metadata_query, include_context=True) - root = extract_and_parse_xml(response) type_elem = root.find('.//type') - desc_elem = root.find('.//description') + summary_elem = root.find('.//summary') exports_elem = root.find('.//exports') - + imports_elem = root.find('.//imports') # Added imports_elem file_type = type_elem.text.strip( ) if type_elem is not None and type_elem.text else "unknown" - description = desc_elem.text.strip( - ) if desc_elem is not None and desc_elem.text else "No description available" + summary = summary_elem.text.strip( + ) if summary_elem is not None and summary_elem.text else "No summary available" exports = exports_elem.text.strip( ) if exports_elem is not None and exports_elem.text else "" - + imports = imports_elem.text.strip( + ) if imports_elem is not None and imports_elem.text else "" # Added imports print_success(f"Processed: {filename}") - return filename, file_type, description, exports + # Added imports to return tuple + return filename, file_type, summary, exports, imports except Exception as e: print_error(f"Error processing {filename}: {e}") - return filename, "unknown", f"Error: {e}", "" + # Added empty string for imports in error case + return filename, "unknown", f"Error: {e}", "", "" async def process_files(files, project_context, folder_structure): diff --git a/src/drd/metadata/updater.py b/src/drd/metadata/updater.py index 4244d5f..0099200 100644 --- a/src/drd/metadata/updater.py +++ b/src/drd/metadata/updater.py @@ -70,26 +70,21 @@ async def update_metadata_with_dravid_async(meta_description, current_dir): ) print_success( f"Updated metadata for file: {found_filename}") + + # Handle external dependencies + metadata = file.find('metadata') + if metadata is not None: + external_deps = metadata.find('external_dependencies') + if external_deps is not None: + for dep in external_deps.findall('dependency'): + metadata_manager.add_external_dependency( + dep.text.strip()) else: print_warning(f"Could not analyze file: {found_filename}") except Exception as e: print_error(f"Error processing {found_filename}: {str(e)}") - # After processing all files, update the environment info - all_languages = set(file['type'] for file in metadata_manager.metadata['key_files'] - if file['type'] not in ['binary', 'unknown']) - if all_languages: - primary_language = max(all_languages, key=lambda x: sum( - 1 for file in metadata_manager.metadata['key_files'] if file['type'] == x)) - other_languages = list(all_languages - {primary_language}) - metadata_manager.update_environment_info( - primary_language=primary_language, - other_languages=other_languages, - primary_framework=metadata_manager.metadata['environment']['primary_framework'], - runtime_version=metadata_manager.metadata['environment']['runtime_version'] - ) - print_success("Metadata update completed.") except Exception as e: print_error(f"Error parsing dravid's response: {str(e)}") diff --git a/src/drd/prompts/file_metada_desc_prompts.py b/src/drd/prompts/file_metada_desc_prompts.py index dfecddf..e9fa938 100644 --- a/src/drd/prompts/file_metada_desc_prompts.py +++ b/src/drd/prompts/file_metada_desc_prompts.py @@ -11,23 +11,32 @@ def get_file_metadata_prompt(filename, content, project_context, folder_structur so it can be used by an AI coding assistant in future for reference. Based on the file content, project context, and the current folder structure, -please generate appropriate metadata for this file. +please generate appropriate metadata for this file -If this file appears to be a dependency management file (like package.json, requirements.txt, Cargo.toml, etc.), -provide a list of external dependencies. +Guidelines: +1. 'path' should be the full path of the file within the project. +2. 'type' should be the programming language or file type (e.g., "typescript", "python", "json"). +3. 'summary' should be a concise description of the file's main purpose. +4. 'exports' should list the exported items with their types (fun: for functions, class: for classes, var: for variables etc). +5. 'imports' should list imports from other project files, including the path and imported item. +6. 'external_dependencies' should list external dependencies for dependency management files if the current file appears to +be deps management file (package.json, requirements.txt, Cargo.toml etc). +7. If there are no exports, imports, or external dependencies, use an empty array []. +8. Ensure all fields are present in the JSON object. +9. If there are no exports, use None instead of an empty tag. +10. If there are no imports, use None instead of an empty tag. +11. If there are no external dependencies, omit the tag entirely. +12. Ensure that all other tags (type, description, file_category, exports, imports) are always present and non-empty. -If it's a code file, provide a summary, list of exports (functions, classes, or variables available for importing), -and a list of imports from other project files. Respond with an XML structure containing the metadata: file_type - Description based on the file's contents, project context, and folder structure - code_file or dependency_file + summary based on the file's contents, project context, and folder structure fun:functionName,class:ClassName,var:variableName - path/to/file:importedName + path/to/file name1@version1 @@ -36,9 +45,31 @@ def get_file_metadata_prompt(filename, content, project_context, folder_structur +examples: + + + src/components/Layout.tsx + typescript + Main layout component + fun:Layout + src/components/Footer + + + + + + package.json + json + Node.js project configuration and dependencies + None + None + + react@18.2.0 + next@13.4.1 + typescript@5.0.4 + + + + Respond strictly only with the XML response as it will be used for parsing, no other extra words. -If there are no exports, use None instead of an empty tag. -If there are no imports, use None instead of an empty tag. -If there are no external dependencies, omit the tag entirely. -Ensure that all other tags (type, description, file_category, exports, imports) are always present and non-empty. """ diff --git a/tests/cli/query/test_dynamic_command_handler.py b/tests/cli/query/test_dynamic_command_handler.py index fe31c6a..8272b86 100644 --- a/tests/cli/query/test_dynamic_command_handler.py +++ b/tests/cli/query/test_dynamic_command_handler.py @@ -1,3 +1,4 @@ +import asyncio import unittest from unittest.mock import patch, MagicMock, call, mock_open import xml.etree.ElementTree as ET @@ -12,6 +13,7 @@ ) +# Change back to unittest.TestCase class TestDynamicCommandHandler(unittest.TestCase): def setUp(self): @@ -78,19 +80,25 @@ def test_handle_file_operation(self, mock_update_metadata, mock_print_success, m # cmd, self.metadata_manager, self.executor) @patch('drd.cli.query.dynamic_command_handler.generate_file_description') - def test_update_file_metadata(self, mock_generate_description): + async def test_update_file_metadata(self, mock_generate_description): cmd = {'filename': 'test.txt', 'content': 'Test content'} - mock_generate_description.return_value = ( - 'python', 'Test file', ['test_function']) - - update_file_metadata(cmd, self.metadata_manager, self.executor) - - self.metadata_manager.get_project_context.assert_called_once() - self.executor.get_folder_structure.assert_called_once() - mock_generate_description.assert_called_once_with( - 'test.txt', 'Test content', self.metadata_manager.get_project_context(), self.executor.get_folder_structure()) + mock_file_info = { + 'path': 'test.txt', + 'type': 'python', + 'summary': 'Test file', + 'exports': ['test_function'], + 'imports': [], + 'xml_response': '' + } + self.metadata_manager.analyze_file.return_value = mock_file_info + + await update_file_metadata(cmd, self.metadata_manager, self.executor) + + self.metadata_manager.analyze_file.assert_called_once_with('test.txt') self.metadata_manager.update_file_metadata.assert_called_once_with( - 'test.txt', 'python', 'Test content', 'Test file', ['test_function']) + 'test.txt', 'python', 'Test content', 'Test file', [ + 'test_function'], [] + ) @patch('drd.cli.query.dynamic_command_handler.print_error') @patch('drd.cli.query.dynamic_command_handler.print_info') diff --git a/tests/metadata/test_project_metadata.py b/tests/metadata/test_project_metadata.py index 64022eb..fedbe19 100644 --- a/tests/metadata/test_project_metadata.py +++ b/tests/metadata/test_project_metadata.py @@ -77,7 +77,7 @@ async def test_analyze_file(self, mock_file, mock_api_call): python - A simple Python script + A simple Python script None None diff --git a/tests/metadata/test_rate_limit_handler.py b/tests/metadata/test_rate_limit_handler.py index ce69c87..6dfadb1 100644 --- a/tests/metadata/test_rate_limit_handler.py +++ b/tests/metadata/test_rate_limit_handler.py @@ -50,14 +50,14 @@ async def test_rate_limiter(self): @patch('drd.metadata.rate_limit_handler.call_dravid_api_with_pagination') @patch('drd.metadata.rate_limit_handler.extract_and_parse_xml') async def test_process_single_file(self, mock_extract_xml, mock_call_api): - mock_call_api.return_value = "pythonA test filetest_function" + mock_call_api.return_value = "pythonA test filetest_functionos,sys" mock_root = ET.fromstring(mock_call_api.return_value) mock_extract_xml.return_value = mock_root result = await process_single_file("test.py", "print('Hello')", "Test project", {"test.py": "file"}) self.assertEqual(result, ("test.py", "python", - "A test file", "test_function")) + "A test file", "test_function", "os,sys")) mock_call_api.assert_called_once() mock_extract_xml.assert_called_once_with(mock_call_api.return_value) diff --git a/tests/metadata/test_updater.py b/tests/metadata/test_updater.py index a30752e..05775ba 100644 --- a/tests/metadata/test_updater.py +++ b/tests/metadata/test_updater.py @@ -1,6 +1,7 @@ import unittest from unittest.mock import patch, MagicMock, mock_open import xml.etree.ElementTree as ET +import asyncio from drd.metadata.updater import update_metadata_with_dravid @@ -52,7 +53,7 @@ def test_update_metadata_with_dravid(self, mock_print_error, mock_print_warning, update python - Main Python file + Main Python file main_function os @@ -69,7 +70,7 @@ def test_update_metadata_with_dravid(self, mock_print_error, mock_print_warning, update json - Package configuration file + Package configuration file None None @@ -87,19 +88,30 @@ def test_update_metadata_with_dravid(self, mock_print_error, mock_print_warning, mock_find_file.side_effect = [ '/fake/project/dir/src/main.py', '/fake/project/dir/package.json'] - # Mock file contents - mock_file_contents = { - '/fake/project/dir/src/main.py': "print('Hello, World!')", - '/fake/project/dir/package.json': '{"name": "test-project"}' - } - - def mock_open_file(filename, *args, **kwargs): - return mock_open(read_data=mock_file_contents.get(filename, ""))() - - with patch('builtins.open', mock_open_file): - # Call the function - update_metadata_with_dravid( - self.meta_description, self.current_dir) + # Mock analyze_file method + async def mock_analyze_file(filename): + if filename == '/fake/project/dir/src/main.py': + return { + 'path': '/fake/project/dir/src/main.py', + 'type': 'python', + 'summary': "print('Hello, World!')", + 'exports': ['main_function'], + 'imports': ['os'] + } + elif filename == '/fake/project/dir/package.json': + return { + 'path': '/fake/project/dir/package.json', + 'type': 'json', + 'summary': '{"name": "test-project"}', + 'exports': [], + 'imports': [] + } + return None + + mock_metadata_manager.return_value.analyze_file = mock_analyze_file + + # Call the function + update_metadata_with_dravid(self.meta_description, self.current_dir) # Assertions mock_metadata_manager.assert_called_once_with(self.current_dir) @@ -110,11 +122,11 @@ def mock_open_file(filename, *args, **kwargs): # Check if metadata was correctly updated and removed mock_metadata_manager.return_value.update_file_metadata.assert_any_call( - '/fake/project/dir/src/main.py', 'python', "print('Hello, World!')", 'Main Python file', [ + '/fake/project/dir/src/main.py', 'python', "print('Hello, World!')", [ 'main_function'], ['os'] ) mock_metadata_manager.return_value.update_file_metadata.assert_any_call( - '/fake/project/dir/package.json', 'json', '{"name": "test-project"}', 'Package configuration file', [ + '/fake/project/dir/package.json', 'json', '{"name": "test-project"}', [ ], [] ) mock_metadata_manager.return_value.remove_file_metadata.assert_called_once_with(