├── src └── readmex │ ├── __init__.py │ ├── utils │ ├── __init__.py │ ├── logo_generator.py │ ├── file_handler.py │ ├── cli.py │ ├── language_analyzer.py │ ├── dependency_analyzer.py │ └── model_client.py │ ├── images │ ├── logo.png │ └── screenshot.png │ ├── __main__.py │ ├── config │ ├── language_mapping.json │ ├── ignore_patterns.json │ └── dependency_config.json │ ├── templates │ └── BLANK_README.md │ └── config.py ├── tests ├── test_core_basic_info.py ├── test_llm.py ├── test_generate.py ├── test_error_handling.py ├── test_image_generation.py ├── test_t2i_config.py ├── test_azure_openai_simple.py ├── test_auto_description.py ├── test_core_threading.py ├── test_extended_auto_generation.py ├── test_logo_generator.py └── test_complete_dependency_analysis.py ├── images ├── flow.png ├── logo.png ├── poster.png └── group_qr.png ├── requirements.txt ├── source.env ├── config_template.json ├── .github └── workflows │ └── publish.yml ├── LICENSE.txt ├── pyproject.toml ├── .gitignore ├── .flake8 ├── README_CN.md └── README.md /src/readmex/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/readmex/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_core_basic_info.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aibox22/readmeX/HEAD/images/flow.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aibox22/readmeX/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aibox22/readmeX/HEAD/images/poster.png -------------------------------------------------------------------------------- /images/group_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aibox22/readmeX/HEAD/images/group_qr.png -------------------------------------------------------------------------------- /src/readmex/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aibox22/readmeX/HEAD/src/readmex/images/logo.png -------------------------------------------------------------------------------- /src/readmex/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aibox22/readmeX/HEAD/src/readmex/images/screenshot.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv>=0.15.0 2 | numpy>=1.21.0 3 | openai>=0.11.0 4 | rich>=10.0.0 5 | requests>=2.25.0 6 | pytest>=6.0.0 7 | flake8>=7.0.0 -------------------------------------------------------------------------------- /src/readmex/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | readmex package main entry point. 4 | Allows running the package as a module with: python -m readmex 5 | """ 6 | 7 | from readmex.utils.cli import main 8 | 9 | if __name__ == '__main__': 10 | main() -------------------------------------------------------------------------------- /source.env: -------------------------------------------------------------------------------- 1 | # LLM 配置 2 | LLM_BASE_URL=https://api.deepbricks.ai/v1/ 3 | LLM_API_KEY=sk-upX1ZDkrORvWcQjdnB8h2CdrjLIPcsqqHqOl7titUUE7p5o0 4 | LLM_MODEL_NAME=gpt-4o 5 | 6 | # 文生图配置 7 | T2I_BASE_URL=https://api.deepbricks.ai/v1/ 8 | T2I_API_KEY=sk-upX1ZDkrORvWcQjdnB8h2CdrjLIPcsqqHqOl7titUUE7p5o0 9 | T2I_MODEL_NAME=dall-e-3 10 | 11 | # Embedding 配置 12 | EMBEDDING_BASE_URL=https://api.deepbricks.ai/v1/ 13 | EMBEDDING_API_KEY=sk-upX1ZDkrORvWcQjdnB8h2CdrjLIPcsqqHqOl7titUUE7p5o0 14 | EMBEDDING_MODEL_NAME=text-embedding-3-small 15 | LOCAL_EMBEDDING=false 16 | -------------------------------------------------------------------------------- /config_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "LLM_API_KEY": "", 3 | "LLM_BASE_URL": "https://api.openai.com/v1", 4 | "LLM_MODEL_NAME": "gpt-3.5-turbo", 5 | "T2I_API_KEY": "", 6 | "T2I_BASE_URL": "https://api.openai.com/v1", 7 | "T2I_MODEL_NAME": "dall-e-3", 8 | "EMBEDDING_API_KEY": "", 9 | "EMBEDDING_BASE_URL": "https://api.openai.com/v1", 10 | "EMBEDDING_MODEL_NAME": "text-embedding-3-small", 11 | "LOCAL_EMBEDDING": "false", 12 | "MAX_WORKERS": "10", 13 | "GITHUB_USERNAME": "", 14 | "TWITTER_HANDLE": "", 15 | "LINKEDIN_USERNAME": "", 16 | "EMAIL": "" 17 | } -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package to PyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | 8 | jobs: 9 | build-and-publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.9' 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install build twine 25 | 26 | - name: Build package 27 | run: python -m build 28 | 29 | - name: Publish package 30 | run: twine upload --repository pypi --skip-existing dist/* --verbose 31 | env: 32 | TWINE_USERNAME: __token__ 33 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 lintaojlu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/test_llm.py: -------------------------------------------------------------------------------- 1 | # tests/test_model_client.py 2 | # 测试 ModelClient 类的 get_answer 和 get_image 方法 3 | 4 | import pytest 5 | from pathlib import Path 6 | import sys 7 | root_dir = Path(__file__).parent.parent 8 | sys.path.append(str(root_dir)) 9 | from src.readmex.utils.model_client import ModelClient 10 | 11 | # 测试 get_answer 方法 12 | def test_get_answer(): 13 | # 实例化 ModelClient 14 | client = ModelClient() 15 | # 构造问题 16 | question = "你好,介绍一下你自己" 17 | # 调用 get_answer 获取回复 18 | answer = client.get_answer(question) 19 | print("get_answer 返回:", answer) 20 | # 断言返回内容不为空 21 | assert answer is not None and len(answer) > 0 22 | 23 | # 测试 get_image 方法 24 | # @pytest.mark.skip(reason="生成图片会消耗额度,调试时可去掉本行") 25 | def test_get_image(): 26 | # 实例化 ModelClient 27 | client = ModelClient() 28 | # 设置图片生成的 prompt 29 | prompt = "一只可爱的卡通猫,蓝色背景" 30 | # 调用 get_image 获取图片内容 31 | img_result = client.get_image(prompt) 32 | print("get_image 返回结果:", img_result) 33 | # 断言返回结果包含url或content 34 | assert img_result is not None 35 | assert "url" in img_result or "content" in img_result 36 | 37 | # 测试配置信息获取 38 | def test_get_current_settings(): 39 | # 实例化 ModelClient 40 | client = ModelClient() 41 | # 获取当前设置 42 | settings = client.get_current_settings() 43 | print("当前设置:", settings) 44 | # 断言设置不为空 45 | assert settings is not None 46 | assert "llm_model_name" in settings 47 | assert "t2i_model_name" in settings -------------------------------------------------------------------------------- /tests/test_generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify the generate method works with project_path parameter 4 | and configuration loading logic. 5 | """ 6 | 7 | import sys 8 | import os 9 | sys.path.insert(0, 'src') 10 | 11 | from readmex.core import readmex 12 | from rich.console import Console 13 | 14 | def test_generate(): 15 | console = Console() 16 | console.print("[bold cyan]Testing readmex generate method...[/bold cyan]") 17 | 18 | try: 19 | # Create generator instance 20 | generator = readmex() 21 | 22 | # Test with current directory 23 | current_dir = os.getcwd() 24 | console.print(f"[green]Testing with project path: {current_dir}[/green]") 25 | 26 | # This should now work without the "takes 1 positional argument but 2 were given" error 27 | generator.generate(current_dir) 28 | 29 | console.print("[bold green]✅ Test passed! Generate method accepts project_path parameter.[/bold green]") 30 | 31 | except TypeError as e: 32 | if "takes 1 positional argument but 2 were given" in str(e): 33 | console.print("[bold red]❌ Test failed! Generate method still has parameter issue.[/bold red]") 34 | console.print(f"[red]Error: {e}[/red]") 35 | else: 36 | console.print(f"[yellow]Different TypeError: {e}[/yellow]") 37 | except Exception as e: 38 | console.print(f"[yellow]Other error (expected during testing): {e}[/yellow]") 39 | console.print("[green]✅ Parameter issue is fixed, but other configuration issues exist.[/green]") 40 | 41 | if __name__ == "__main__": 42 | test_generate() -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "readmex" 7 | version = "0.3.0" 8 | authors = [ 9 | { name="stone91", email="m370025263@gmail.com" }, 10 | { name="lintao", email="lint22@mails.tsinghua.edu.cn"}, 11 | ] 12 | description = "A tool to automatically generate README files for your projects." 13 | readme = "README.md" 14 | requires-python = ">=3.8" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ] 20 | dependencies = [ 21 | "python-dotenv>=0.15.0", 22 | "numpy>=1.21.0", 23 | "openai>=0.11.0", 24 | "rich>=10.0.0", 25 | "requests>=2.25.0", 26 | "pytest>=6.0.0", 27 | "sentence-transformers>=2.0.0", 28 | "faiss-cpu>=1.7.0", 29 | "flake8>=7.0.0" 30 | ] 31 | 32 | [tool.setuptools.packages.find] 33 | where = ["src"] 34 | 35 | [tool.setuptools.package-data] 36 | readmex = ["**/*.md", "**/*.json", "**/*.png"] 37 | 38 | [project.scripts] 39 | readmex = "readmex.utils.cli:main" 40 | 41 | [project.urls] 42 | "Homepage" = "https://github.com/aibox22/readmex" 43 | "Bug Tracker" = "https://github.com/aibox22/readmex/issues" 44 | 45 | [tool.flake8] 46 | max-line-length = 120 47 | ignore = ["E501", "W503", "F401", "E402", "F841", "E203", "W504", "W293", "E301", "E302", "W291", "W292", "F541", "F811", "E305", "E128", "E127", "E129", "E226", "E722", "E303", "E261", "F824", "E114", "E116", "F403", "F405", "E731", "E704", "W601", "W602", "W603", "W604"] 48 | exclude = [".git", "__pycache__", ".venv", ".env", "build", "dist", "*.egg-info"] 49 | per-file-ignores = [ 50 | "tests/*.py:F401,E402,F841,E501,W293,E301,E302,W291,W292,F541,E305", 51 | "__init__.py:F401" 52 | ] 53 | -------------------------------------------------------------------------------- /src/readmex/config/language_mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "python": [".py", ".pyx", ".pyi", ".pyw", ".ipynb"], 3 | "javascript": [".js", ".jsx", ".mjs"], 4 | "typescript": [".ts", ".tsx"], 5 | "java": [".java", ".jar", ".class"], 6 | "c": [".c", ".o", ".so", ".dll", ".dylib"], 7 | "cpp": [".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".h++", ".o", ".so", ".dll", ".dylib"], 8 | "csharp": [".cs", ".dll", ".exe"], 9 | "go": [".go", ".exe"], 10 | "rust": [".rs", ".exe", ".dll", ".so", ".dylib"], 11 | "php": [".php", ".phtml"], 12 | "ruby": [".rb", ".erb", ".rake"], 13 | "swift": [".swift"], 14 | "kotlin": [".kt", ".kts"], 15 | "scala": [".scala"], 16 | "html": [".html", ".htm"], 17 | "css": [".css"], 18 | "scss": [".scss"], 19 | "sass": [".sass"], 20 | "less": [".less"], 21 | "stylus": [".styl"], 22 | "pug": [".pug", ".jade"], 23 | "handlebars": [".hbs", ".handlebars"], 24 | "mustache": [".mustache"], 25 | "twig": [".twig"], 26 | "smarty": [".tpl"], 27 | "jinja": [".j2", ".jinja2"], 28 | "vue": [".vue"], 29 | "r": [".r", ".R"], 30 | "matlab": [".m", ".mat"], 31 | "perl": [".pl", ".pm"], 32 | "lua": [".lua"], 33 | "dart": [".dart"], 34 | "fsharp": [".fs"], 35 | "visualbasic": [".vb"], 36 | "assembly": [".asm", ".s", ".S"], 37 | "objectivec": [".m", ".mm"], 38 | "haskell": [".hs"], 39 | "erlang": [".erl"], 40 | "elixir": [".ex", ".exs"], 41 | "clojure": [".clj", ".cljs"], 42 | "coffeescript": [".coffee"], 43 | "powershell": [".ps1", ".psm1"], 44 | "shell": [".sh", ".bash", ".zsh", ".fish"], 45 | "batch": [".bat", ".cmd"], 46 | "solidity": [".sol"], 47 | "nix": [".nix"], 48 | "terraform": [".tf", ".tfvars"], 49 | "dockerfile": ["Dockerfile", ".dockerfile"], 50 | "makefile": ["Makefile", "makefile", ".mk"], 51 | "cmake": [".cmake", "CMakeLists.txt"], 52 | "gradle": [".gradle"], 53 | "maven": [".pom"], 54 | "jupyter": [".ipynb"], 55 | "vim": [".vim"], 56 | "emacs": [".el"], 57 | "protobuf": [".proto"], 58 | "graphql": [".graphql", ".gql"], 59 | "webassembly": [".wasm", ".wat"] 60 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | website/ 13 | tests/ 14 | .rag_cache/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | -pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | .pytest_cache/ 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # IPython 75 | profile_default/ 76 | ipython_config.py 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # PEP 582; used by Poetry, pdm and others 82 | __pypackages__/ 83 | 84 | # Celery stuff 85 | celerybeat-schedule 86 | celerybeat.pid 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | .env 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | # macOS system files 120 | .DS_Store 121 | .DS_Store? 122 | ._* 123 | .Spotlight-V100 124 | .Trashes 125 | ehthumbs.db 126 | Thumbs.db 127 | 128 | # readmex_output 129 | *output*/ 130 | # PyCharm/IntelliJ IDEA 131 | .idea/ 132 | .vscode/launch.json 133 | 134 | # Local configurations 135 | .readmex -------------------------------------------------------------------------------- /tests/test_error_handling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify improved error handling and configuration display 4 | """ 5 | 6 | import sys 7 | import os 8 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) 9 | 10 | from rich.console import Console 11 | from readmex.config import load_config, get_config_sources 12 | from readmex.utils.cli import main 13 | 14 | def test_error_handling(): 15 | """Test the improved error handling with configuration display""" 16 | console = Console() 17 | 18 | try: 19 | # Simulate a connection error 20 | raise ConnectionError("Connection error") 21 | 22 | except Exception as e: 23 | console.print(f"[bold red]An error occurred: {e}[/bold red]") 24 | 25 | # Show configuration information to help with debugging 26 | try: 27 | config = load_config() 28 | sources = get_config_sources() 29 | if config and sources: 30 | # Show configuration source info once 31 | console.print("\n[yellow]Configuration loaded from:[/yellow]") 32 | source_files = set(sources.values()) 33 | for source_file in source_files: 34 | if "Environment Variable" not in source_file: 35 | console.print(f"[yellow] • {source_file}[/yellow]") 36 | 37 | # Show configuration table with actual values 38 | from rich.table import Table 39 | table = Table(title="[bold cyan]Current Configuration[/bold cyan]") 40 | table.add_column("Variable", style="cyan") 41 | table.add_column("Value", style="green") 42 | 43 | # Only show non-sensitive configuration values 44 | display_keys = ["llm_model_name", "t2i_model_name", "llm_base_url", "t2i_base_url", 45 | "github_username", "twitter_handle", "linkedin_username", "email"] 46 | 47 | for key in display_keys: 48 | if key in config and config[key]: 49 | value = config[key] 50 | # Mask API keys for security 51 | if "api_key" in key.lower(): 52 | value = "***" + value[-4:] if len(value) > 4 else "***" 53 | table.add_row(key, value) 54 | 55 | console.print(table) 56 | except Exception as config_error: 57 | console.print(f"[red]Could not load configuration: {config_error}[/red]") 58 | 59 | if __name__ == "__main__": 60 | test_error_handling() -------------------------------------------------------------------------------- /tests/test_image_generation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify fixed image generation functionality 4 | """ 5 | 6 | import sys 7 | import os 8 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) 9 | 10 | from rich.console import Console 11 | from readmex.utils.model_client import ModelClient 12 | 13 | def test_image_generation(): 14 | """Test the fixed image generation functionality""" 15 | console = Console() 16 | 17 | console.print("[bold cyan]=== Testing Fixed Image Generation ===[/bold cyan]") 18 | 19 | try: 20 | # Create model client 21 | client = ModelClient() 22 | 23 | # Display current settings 24 | console.print("\n[yellow]Current Settings:[/yellow]") 25 | settings = client.get_current_settings() 26 | for key, value in settings.items(): 27 | console.print(f" {key}: {value}") 28 | 29 | # Test image generation with a simple prompt 30 | console.print("\n[yellow]Testing image generation...[/yellow]") 31 | prompt = "A simple geometric logo with blue and white colors" 32 | 33 | console.print(f"[cyan]Prompt: {prompt}[/cyan]") 34 | 35 | # Generate image 36 | result = client.get_image(prompt) 37 | 38 | if result and "url" in result: 39 | console.print("[bold green]✓ Image generation successful![/bold green]") 40 | console.print(f"[green]Image URL: {result['url']}[/green]") 41 | if result.get('content'): 42 | console.print(f"[green]Content size: {len(result['content'])} bytes[/green]") 43 | else: 44 | console.print("[yellow]⚠️ Image URL generated but content download failed[/yellow]") 45 | else: 46 | console.print("[red]✗ Image generation failed[/red]") 47 | 48 | except Exception as e: 49 | console.print(f"[red]✗ Test failed with error: {e}[/red]") 50 | 51 | # Check if it's the specific model parameter error 52 | if "InvalidParameter" in str(e) and "model" in str(e): 53 | console.print("\n[yellow]This appears to be a model parameter issue.[/yellow]") 54 | console.print("[yellow]The fix should have resolved this. Please check:[/yellow]") 55 | console.print("1. Model name compatibility with your API provider") 56 | console.print("2. API endpoint configuration") 57 | console.print("3. API key validity") 58 | 59 | return False 60 | 61 | return True 62 | 63 | if __name__ == "__main__": 64 | success = test_image_generation() 65 | if success: 66 | print("\n🎉 Image generation test completed successfully!") 67 | else: 68 | print("\n❌ Image generation test failed.") -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # Maximum line length 3 | max-line-length = 120 4 | 5 | # Ignore specific error codes 6 | ignore = 7 | # E501: Line too long (handled by max-line-length) 8 | E501, 9 | # W503: Line break before binary operator (conflicts with W504) 10 | W503, 11 | # F401: Module imported but unused (common in test files) 12 | F401, 13 | # E402: Module level import not at top of file (needed for sys.path manipulation) 14 | E402, 15 | # F841: Local variable assigned but never used (common in tests) 16 | F841, 17 | # E203: Whitespace before ':' (conflicts with black formatter) 18 | E203, 19 | # W504: Line break after binary operator 20 | W504, 21 | # W293: Blank line contains whitespace 22 | W293, 23 | # E301: Expected 1 blank line, found 0 24 | E301, 25 | # E302: Expected 2 blank lines, found 1 26 | E302, 27 | # W291: Trailing whitespace (common formatting issue) 28 | W291, 29 | # W292: No newline at end of file 30 | W292, 31 | # F541: f-string is missing placeholders (often used for consistency) 32 | F541, 33 | # F811: Redefinition of unused import (common with conditional imports) 34 | F811, 35 | # E305: Expected 2 blank lines after class or function definition, found 1 36 | E305, 37 | # E128: Continuation line under-indented for visual indent 38 | E128, 39 | # E127: Continuation line over-indented for visual indent 40 | E127, 41 | # E129: Visually indented line with same indent as next logical line 42 | E129, 43 | # E226: Missing whitespace around arithmetic operator 44 | E226, 45 | # E722: Do not use bare 'except' (sometimes necessary for broad exception handling) 46 | E722, 47 | # E303: Too many blank lines 48 | E303, 49 | # E261: At least two spaces before inline comment 50 | E261, 51 | # F824: Global variable is unused 52 | F824, 53 | # E114: Indentation is not a multiple of 4 (comment) 54 | E114, 55 | # E116: Unexpected indentation (comment) 56 | E116, 57 | # F403: 'from module import *' used; unable to detect undefined names 58 | F403, 59 | # F405: Name may be undefined, or defined from star imports 60 | F405, 61 | # E731: Do not assign a lambda expression, use a def 62 | E731, 63 | # E704: Multiple statements on one line (def) 64 | E704, 65 | # W601: .has_key() is deprecated, use 'in' 66 | W601, 67 | # W602: Deprecated form of raising exception 68 | W602, 69 | # W603: '<>' is deprecated, use '!=' 70 | W603, 71 | # W604: Backticks are deprecated, use 'repr()' 72 | W604 73 | 74 | # Exclude directories and files 75 | exclude = 76 | .git, 77 | __pycache__, 78 | .venv, 79 | .env, 80 | build, 81 | dist, 82 | *.egg-info, 83 | .tox, 84 | .coverage, 85 | htmlcov, 86 | .pytest_cache 87 | 88 | # Per-file ignores for test files 89 | per-file-ignores = 90 | # Test files can have additional relaxed rules 91 | tests/*.py:F401,E402,F841,E501 92 | # Init files often have unused imports 93 | __init__.py:F401 94 | -------------------------------------------------------------------------------- /tests/test_t2i_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to check T2I configuration and model compatibility 4 | """ 5 | 6 | import sys 7 | import os 8 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) 9 | 10 | from rich.console import Console 11 | from readmex.config import load_config, get_t2i_config 12 | from readmex.utils.model_client import ModelClient 13 | 14 | def test_t2i_config(): 15 | """Test T2I configuration and model compatibility""" 16 | console = Console() 17 | 18 | console.print("[bold cyan]=== T2I Configuration Check ===[/bold cyan]") 19 | 20 | # Load and display configuration 21 | config = load_config() 22 | t2i_config = get_t2i_config() 23 | 24 | console.print("\n[yellow]Current T2I Configuration:[/yellow]") 25 | for key, value in t2i_config.items(): 26 | if "api_key" in key: 27 | masked_value = "***" + value[-4:] if value and len(value) > 4 else "***" 28 | console.print(f" {key}: {masked_value}") 29 | else: 30 | console.print(f" {key}: {value}") 31 | 32 | # Check model compatibility 33 | model_name = t2i_config.get("model_name", "dall-e-3") 34 | base_url = t2i_config.get("base_url", "https://api.openai.com/v1") 35 | 36 | console.print(f"\n[yellow]Model Analysis:[/yellow]") 37 | console.print(f" Model Name: {model_name}") 38 | console.print(f" Base URL: {base_url}") 39 | 40 | # Check if using custom API endpoint 41 | if "openai.com" not in base_url: 42 | console.print(f"\n[yellow]⚠️ Using custom API endpoint: {base_url}[/yellow]") 43 | console.print("[yellow]Common model compatibility issues:[/yellow]") 44 | console.print(" • Some providers may not support 'dall-e-3' model name") 45 | console.print(" • Try alternative model names like:") 46 | console.print(" - 'dall-e'") 47 | console.print(" - 'text-to-image'") 48 | console.print(" - 'stable-diffusion'") 49 | console.print(" - Check your provider's documentation for supported models") 50 | 51 | # Test model client initialization 52 | console.print("\n[yellow]Testing ModelClient initialization:[/yellow]") 53 | try: 54 | client = ModelClient() 55 | console.print("[green]✓ ModelClient initialized successfully[/green]") 56 | 57 | # Display current settings 58 | settings = client.get_current_settings() 59 | console.print("\n[yellow]ModelClient Settings:[/yellow]") 60 | for key, value in settings.items(): 61 | console.print(f" {key}: {value}") 62 | 63 | except Exception as e: 64 | console.print(f"[red]✗ ModelClient initialization failed: {e}[/red]") 65 | 66 | # Suggest fixes 67 | console.print("\n[bold yellow]=== Suggested Fixes ===[/bold yellow]") 68 | console.print("1. Check if your API provider supports the model name 'dall-e-3'") 69 | console.print("2. Try updating T2I_MODEL_NAME in your configuration:") 70 | console.print(" - For OpenAI-compatible APIs: try 'dall-e' or 'dall-e-2'") 71 | console.print(" - For other providers: check their documentation") 72 | console.print("3. Verify your API endpoint URL is correct") 73 | console.print("4. Test with a simple model name like 'text-to-image'") 74 | 75 | if __name__ == "__main__": 76 | test_t2i_config() -------------------------------------------------------------------------------- /src/readmex/utils/logo_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from rich.console import Console 3 | from readmex.utils.model_client import ModelClient 4 | 5 | def generate_logo(project_dir, descriptions, model_client, console): 6 | """ 7 | Generate project logo image based on project description 8 | 9 | Args: 10 | project_dir: Project directory path 11 | descriptions: Project description information 12 | model_client: Model client instance 13 | console: Console output object 14 | 15 | Returns: 16 | str: Generated logo image path, returns None if failed 17 | """ 18 | console.print("[cyan]🤖 Generating project logo...[/cyan]") 19 | try: 20 | # Create images directory 21 | images_dir = os.path.join(project_dir, "images") 22 | os.makedirs(images_dir, exist_ok=True) 23 | png_path = os.path.join(images_dir, "logo.png") 24 | 25 | # Step 1: Generate logo description prompt based on project description 26 | description_prompt = f"""Based on the following project information, generate a professional logo design description: 27 | 28 | **Project Information**: 29 | {descriptions} 30 | 31 | Please generate a concise, professional logo design description with the following requirements: 32 | 1. A flat, minimalist vector icon with rectangular outline and rounded corners 33 | 2. Contains geometric shapes that reflect the project's core functionality 34 | 3. Uses smooth gradient colors giving it a modern, tech-inspired look 35 | 4. Clean, simple and modern design, suitable as a project logo 36 | 6. Describe in English, within 50 words 37 | 38 | Return only the logo design description focusing on geometric elements and color scheme, no other explanations. 39 | """ 40 | 41 | # Get logo description 42 | try: 43 | logo_description = model_client.get_answer(description_prompt) 44 | except Exception as e: 45 | console.print(f"[red]Failed to get logo description: {e}[/red]") 46 | return None 47 | 48 | # Step 2: Generate image using logo description 49 | image_prompt = f"{logo_description} Don't include any text in the image." 50 | console.print(f"[cyan]Image Prompt: {image_prompt}[/cyan]") 51 | console.print(f"[cyan]Generating image...[/cyan]") 52 | 53 | # Call text-to-image API to generate logo with high quality settings 54 | console.print(f"[cyan]Using high-quality image generation (quality: hd, size: 1024x1024)[/cyan]") 55 | try: 56 | image_result = model_client.get_image(image_prompt) 57 | except Exception as e: 58 | console.print(f"[red]Image generation failed: {e}[/red]") 59 | return None 60 | 61 | if not image_result or not image_result.get("content"): 62 | console.print("[red]Image content is empty, generation failed[/red]") 63 | return None 64 | 65 | # Save image file 66 | with open(png_path, 'wb') as f: 67 | f.write(image_result["content"]) 68 | 69 | console.print(f"[green]✔ Logo image saved to {png_path}[/green]") 70 | console.print(f"[green]✔ Image URL: {image_result['url']}[/green]") 71 | 72 | return png_path 73 | 74 | except Exception as e: 75 | console.print(f"[red]Logo generation failed: {e}[/red]") 76 | return None -------------------------------------------------------------------------------- /tests/test_azure_openai_simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Azure OpenAI Integration Tests for ReadmeX 4 | Simplified version to ensure proper execution 5 | """ 6 | 7 | import os 8 | import sys 9 | import traceback 10 | from pathlib import Path 11 | 12 | # Add the src directory to Python path 13 | src_path = Path(__file__).parent.parent / "src" 14 | sys.path.insert(0, str(src_path)) 15 | 16 | from readmex.utils.model_client import ModelClient 17 | from readmex.config import get_llm_config 18 | 19 | try: 20 | from rich.console import Console 21 | RICH_AVAILABLE = True 22 | except ImportError: 23 | RICH_AVAILABLE = False 24 | class Console: 25 | def print(self, text): 26 | import re 27 | clean_text = re.sub(r'\[.*?\]', '', str(text)) 28 | print(clean_text) 29 | 30 | 31 | def test_azure_openai(): 32 | """Main test function for Azure OpenAI integration""" 33 | console = Console() 34 | 35 | if RICH_AVAILABLE: 36 | console.print("[bold green]🧪 Azure OpenAI Integration Tests[/bold green]") 37 | else: 38 | console.print("🧪 Azure OpenAI Integration Tests") 39 | 40 | console.print("=" * 50) 41 | 42 | test_results = [] 43 | 44 | # Test 1: URL Detection 45 | try: 46 | console.print("\n🔍 Test 1: URL Detection") 47 | client = ModelClient() 48 | 49 | test_urls = [ 50 | ("https://api.openai.com/v1", False), 51 | ("https://test.openai.azure.com", True), 52 | ] 53 | 54 | all_passed = True 55 | for url, expected_azure in test_urls: 56 | is_azure = client._is_azure_openai(url) 57 | passed = is_azure == expected_azure 58 | all_passed &= passed 59 | 60 | status = "✅" if passed else "❌" 61 | provider = "Azure" if is_azure else "Standard" 62 | console.print(f" {status} {url} → {provider}") 63 | 64 | test_results.append(("URL Detection", all_passed)) 65 | 66 | except Exception as e: 67 | console.print(f"❌ URL Detection failed: {e}") 68 | test_results.append(("URL Detection", False)) 69 | 70 | # Test 2: Client Initialization 71 | try: 72 | console.print("\n🔌 Test 2: Client Initialization") 73 | client = ModelClient() 74 | 75 | config = get_llm_config() 76 | 77 | console.print(f" LLM Provider: {'Azure OpenAI' if client.is_llm_azure else 'Standard OpenAI'}") 78 | console.print(f" T2I Provider: {'Azure OpenAI' if client.is_t2i_azure else 'Standard OpenAI'}") 79 | console.print(f" API Key: {config.get('api_key', 'Not set')[:10]}...") 80 | 81 | # Check Azure-specific attributes 82 | success = True 83 | if client.is_llm_azure and hasattr(client, 'llm_deployment'): 84 | console.print(f" ✅ LLM Deployment: {client.llm_deployment}") 85 | elif client.is_llm_azure: 86 | console.print(" ❌ LLM deployment not set") 87 | success = False 88 | 89 | test_results.append(("Client Initialization", success)) 90 | 91 | except Exception as e: 92 | console.print(f"❌ Client Initialization failed: {e}") 93 | test_results.append(("Client Initialization", False)) 94 | 95 | # Test 3: API Call (if API key available) 96 | try: 97 | console.print("\n🧪 Test 3: API Call Test") 98 | config = get_llm_config() 99 | 100 | if not config.get('api_key'): 101 | console.print(" ⏭️ Skipped - No API key configured") 102 | test_results.append(("API Call", True)) 103 | else: 104 | client = ModelClient() 105 | console.print(" 🔄 Testing simple API call...") 106 | 107 | response = client.get_answer("What is 1+1? Answer with just the number.", max_retries=1) 108 | success = response and len(response.strip()) > 0 109 | 110 | if success: 111 | console.print(f" ✅ API call successful: '{response.strip()}'") 112 | else: 113 | console.print(" ❌ API call failed or returned empty response") 114 | 115 | test_results.append(("API Call", success)) 116 | 117 | except Exception as e: 118 | console.print(f" ❌ API call failed: {e}") 119 | test_results.append(("API Call", False)) 120 | 121 | # Summary 122 | console.print("\n" + "=" * 50) 123 | console.print("📊 Test Results Summary:") 124 | 125 | passed = sum(1 for _, success in test_results if success) 126 | total = len(test_results) 127 | 128 | for test_name, success in test_results: 129 | status = "✅ PASS" if success else "❌ FAIL" 130 | console.print(f" {status} {test_name}") 131 | 132 | console.print(f"\nOverall: {passed}/{total} tests passed") 133 | 134 | if passed == total: 135 | console.print("🎉 All tests passed!") 136 | else: 137 | console.print("⚠️ Some tests failed.") 138 | 139 | return passed == total 140 | 141 | 142 | if __name__ == "__main__": 143 | print("🚀 Starting Azure OpenAI Integration Tests...") 144 | try: 145 | success = test_azure_openai() 146 | print(f"\n✅ Tests completed successfully: {success}") 147 | sys.exit(0 if success else 1) 148 | except Exception as e: 149 | print(f"❌ Test execution failed: {e}") 150 | traceback.print_exc() 151 | sys.exit(1) 152 | -------------------------------------------------------------------------------- /tests/test_auto_description.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple test to verify the auto-generated project description method exists and is callable 4 | """ 5 | 6 | import sys 7 | import os 8 | 9 | # Add src to path 10 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) 11 | 12 | def test_method_exists(): 13 | """Test if the _generate_project_description method exists""" 14 | print("=== Testing Auto Project Description Generation ===") 15 | 16 | try: 17 | # Import the core module 18 | from readmex.core import readmex 19 | 20 | # Check if the method exists 21 | if hasattr(readmex, '_generate_project_description'): 22 | print("✅ _generate_project_description method exists") 23 | 24 | # Check method signature 25 | import inspect 26 | sig = inspect.signature(readmex._generate_project_description) 27 | params = list(sig.parameters.keys()) 28 | 29 | expected_params = ['self', 'structure', 'dependencies', 'descriptions'] 30 | if params == expected_params: 31 | print("✅ Method signature is correct") 32 | print(f" Parameters: {params}") 33 | return True 34 | else: 35 | print(f"❌ Method signature mismatch") 36 | print(f" Expected: {expected_params}") 37 | print(f" Found: {params}") 38 | return False 39 | else: 40 | print("❌ _generate_project_description method not found") 41 | return False 42 | 43 | except ImportError as e: 44 | print(f"❌ Import error: {e}") 45 | return False 46 | except Exception as e: 47 | print(f"❌ Unexpected error: {e}") 48 | return False 49 | 50 | def test_code_changes(): 51 | """Test if the code changes were applied correctly""" 52 | print("\n=== Testing Code Changes ===") 53 | 54 | try: 55 | # Read the core.py file to verify changes 56 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py') 57 | 58 | with open(core_file_path, 'r', encoding='utf-8') as f: 59 | content = f.read() 60 | 61 | # Check for key changes 62 | checks = [ 63 | ('_generate_project_description method', '_generate_project_description(self, structure, dependencies, descriptions)'), 64 | ('Auto-generation call in generate method', 'self._generate_project_description(structure, dependencies, descriptions)'), 65 | ('Updated _get_basic_info prompt', 'press Enter to auto-generate'), 66 | ('Updated _get_project_meta_info prompt', 'press Enter to auto-generate') 67 | ] 68 | 69 | all_passed = True 70 | for check_name, check_text in checks: 71 | if check_text in content: 72 | print(f"✅ {check_name}: Found") 73 | else: 74 | print(f"❌ {check_name}: Not found") 75 | all_passed = False 76 | 77 | return all_passed 78 | 79 | except Exception as e: 80 | print(f"❌ Error reading core.py: {e}") 81 | return False 82 | 83 | def test_workflow_logic(): 84 | """Test the workflow logic for empty descriptions""" 85 | print("\n=== Testing Workflow Logic ===") 86 | 87 | try: 88 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py') 89 | 90 | with open(core_file_path, 'r', encoding='utf-8') as f: 91 | content = f.read() 92 | 93 | # Check for the logic in generate method 94 | workflow_checks = [ 95 | 'if not self.config["project_description"]:', 96 | 'self.config["project_description"] = self._generate_project_description(', 97 | 'structure, dependencies, descriptions' 98 | ] 99 | 100 | all_found = True 101 | for check in workflow_checks: 102 | if check in content: 103 | print(f"✅ Workflow logic found: {check[:50]}...") 104 | else: 105 | print(f"❌ Workflow logic missing: {check[:50]}...") 106 | all_found = False 107 | 108 | return all_found 109 | 110 | except Exception as e: 111 | print(f"❌ Error checking workflow logic: {e}") 112 | return False 113 | 114 | if __name__ == "__main__": 115 | print("🧪 Testing Auto Project Description Generation Feature") 116 | print("=" * 60) 117 | 118 | # Run tests 119 | test1_result = test_method_exists() 120 | test2_result = test_code_changes() 121 | test3_result = test_workflow_logic() 122 | 123 | # Summary 124 | print("\n" + "=" * 60) 125 | print("📊 Test Summary") 126 | print(f"Test 1 (Method exists): {'✅ PASSED' if test1_result else '❌ FAILED'}") 127 | print(f"Test 2 (Code changes): {'✅ PASSED' if test2_result else '❌ FAILED'}") 128 | print(f"Test 3 (Workflow logic): {'✅ PASSED' if test3_result else '❌ FAILED'}") 129 | 130 | if test1_result and test2_result and test3_result: 131 | print("\n🎉 All tests passed! Auto project description feature has been implemented correctly.") 132 | print("\n📝 Implementation Summary:") 133 | print(" • Added _generate_project_description() method") 134 | print(" • Modified _get_basic_info() to support auto-generation") 135 | print(" • Modified _get_project_meta_info() to support auto-generation") 136 | print(" • Updated generate() method to call auto-generation when description is empty") 137 | print(" • Auto-generation uses project structure, dependencies, and file descriptions") 138 | else: 139 | print("\n⚠️ Some tests failed. Please check the implementation.") -------------------------------------------------------------------------------- /src/readmex/utils/file_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import List, Iterator 4 | 5 | def find_files( 6 | directory: str, patterns: List[str], ignore_patterns: List[str] 7 | ) -> Iterator[str]: 8 | """Find files matching patterns in a directory, excluding ignored ones.""" 9 | from fnmatch import fnmatch 10 | 11 | def should_ignore_path(path: str, ignore_patterns: List[str]) -> bool: 12 | """Check if a path should be ignored based on gitignore patterns.""" 13 | for pattern in ignore_patterns: 14 | # Handle directory patterns (ending with /) 15 | if pattern.endswith('/'): 16 | dir_pattern = pattern.rstrip('/') 17 | # Check if any part of the path matches the directory pattern 18 | path_parts = path.split(os.sep) 19 | for i in range(len(path_parts)): 20 | # Check if the directory pattern matches at any level 21 | if fnmatch(path_parts[i], dir_pattern): 22 | return True 23 | # Also check if the full path up to this point matches 24 | partial_path = os.sep.join(path_parts[:i+1]) 25 | if fnmatch(partial_path, dir_pattern): 26 | return True 27 | else: 28 | # Regular file/path pattern 29 | if fnmatch(path, pattern): 30 | return True 31 | return False 32 | 33 | for root, dirs, files in os.walk(directory): 34 | # Correctly handle directory pruning 35 | dirs[:] = [d for d in dirs if not should_ignore_path( 36 | os.path.relpath(os.path.join(root, d), directory), ignore_patterns 37 | )] 38 | 39 | for basename in files: 40 | # 获取文件的相对路径 41 | rel_path = os.path.relpath(os.path.join(root, basename), directory) 42 | if should_ignore_path(rel_path, ignore_patterns): 43 | continue 44 | 45 | # 检查文件是否匹配所需的模式 46 | if any(fnmatch(basename, pattern) for pattern in patterns): 47 | yield os.path.join(root, basename) 48 | 49 | def _should_ignore_path(path: str, basename: str, ignore_patterns: List[str], is_dir: bool = False) -> bool: 50 | """ 51 | 检查路径是否应该被忽略 52 | 53 | Args: 54 | path: 相对路径 55 | basename: 文件或目录名 56 | ignore_patterns: 忽略模式列表 57 | is_dir: 是否为目录 58 | 59 | Returns: 60 | True 如果应该被忽略 61 | """ 62 | from fnmatch import fnmatch 63 | 64 | for ignore in ignore_patterns: 65 | # 处理以 / 结尾的模式(专门用于目录) 66 | if ignore.endswith('/'): 67 | dir_pattern = ignore[:-1] # 去掉末尾的 / 68 | if is_dir and (fnmatch(basename, dir_pattern) or fnmatch(path, dir_pattern)): 69 | return True 70 | else: 71 | # 普通模式匹配 72 | if (fnmatch(path, ignore) or 73 | fnmatch(basename, ignore) or 74 | (is_dir and fnmatch(f"{path}/", ignore)) or 75 | (is_dir and fnmatch(f"{basename}/", ignore))): 76 | return True 77 | 78 | return False 79 | 80 | 81 | def get_project_structure(directory: str, ignore_patterns: List[str]) -> str: 82 | """Generate a string representing the project structure.""" 83 | from fnmatch import fnmatch 84 | 85 | def should_ignore_path_local(path: str, ignore_patterns: List[str]) -> bool: 86 | """Check if a path should be ignored based on gitignore patterns.""" 87 | for pattern in ignore_patterns: 88 | # Handle directory patterns (ending with /) 89 | if pattern.endswith('/'): 90 | dir_pattern = pattern.rstrip('/') 91 | # Check if any part of the path matches the directory pattern 92 | path_parts = path.split(os.sep) 93 | for i in range(len(path_parts)): 94 | # Check if the directory pattern matches at any level 95 | if fnmatch(path_parts[i], dir_pattern): 96 | return True 97 | # Also check if the full path up to this point matches 98 | partial_path = os.sep.join(path_parts[:i+1]) 99 | if fnmatch(partial_path, dir_pattern): 100 | return True 101 | else: 102 | # Regular file/path pattern 103 | if fnmatch(path, pattern): 104 | return True 105 | return False 106 | lines = [] 107 | 108 | for root, dirs, files in os.walk(directory, topdown=True): 109 | rel_root = os.path.relpath(root, directory) 110 | if rel_root == '.': 111 | rel_root = '' 112 | 113 | # Filter out ignored directories 114 | dirs[:] = [d for d in dirs if not should_ignore_path_local(os.path.join(rel_root, d) if rel_root else d, ignore_patterns)] 115 | 116 | # 过滤文件 - 使用新的忽略逻辑 117 | filtered_files = [] 118 | for f in files: 119 | file_path = os.path.join(rel_root, f) if rel_root else f 120 | if not _should_ignore_path(file_path, f, ignore_patterns, is_dir=False): 121 | filtered_files.append(f) 122 | 123 | # 添加当前目录到输出(如果不是根目录) 124 | if rel_root: 125 | level = rel_root.count(os.sep) 126 | indent = " " * level 127 | lines.append(f"{indent}├── {os.path.basename(root)}/") 128 | else: 129 | level = -1 130 | lines.append(f"{os.path.basename(directory)}/") 131 | 132 | # 添加文件到输出 133 | sub_indent = " " * (level + 1) 134 | for f in sorted(filtered_files): 135 | lines.append(f"{sub_indent}├── {f}") 136 | 137 | return "\n".join(lines) 138 | 139 | def load_gitignore_patterns(project_dir: str) -> List[str]: 140 | """Load patterns from .gitignore file.""" 141 | gitignore_path = Path(project_dir) / ".gitignore" 142 | if gitignore_path.exists(): 143 | with open(gitignore_path, "r") as f: 144 | return [ 145 | line.strip() 146 | for line in f 147 | if line.strip() and not line.startswith("#") 148 | ] 149 | return [] -------------------------------------------------------------------------------- /src/readmex/config/ignore_patterns.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": [ 3 | ".git", 4 | "__pycache__", 5 | "node_modules", 6 | ".venv", 7 | "venv", 8 | "env", 9 | ".pytest_cache", 10 | ".idea", 11 | ".vscode", 12 | "build", 13 | "dist", 14 | "target", 15 | "bin", 16 | "obj", 17 | ".DS_Store", 18 | ".mypy_cache", 19 | "coverage", 20 | ".coverage", 21 | "htmlcov", 22 | ".tox", 23 | ".eggs", 24 | "*.egg-info", 25 | "*.egg", 26 | ".pytest_cache", 27 | ".cache", 28 | ".next", 29 | ".nuxt", 30 | "out", 31 | ".output", 32 | ".vercel", 33 | ".sass-cache", 34 | ".parcel-cache", 35 | ".eslintcache", 36 | ".stylelintcache", 37 | ".babel-cache", 38 | ".nyc_output", 39 | "coverage", 40 | "lib-cov", 41 | "lcov.info", 42 | "xunit.xml", 43 | "*.tsbuildinfo", 44 | ".turbo", 45 | ".swc", 46 | ".babel", 47 | ".rollup.cache", 48 | ".esbuild", 49 | ".vite", 50 | ".nuxt", 51 | ".next", 52 | "dist-ssr", 53 | "*.local", 54 | ".env.local", 55 | ".env.development.local", 56 | ".env.test.local", 57 | ".env.production.local", 58 | "npm-debug.log*", 59 | "yarn-debug.log*", 60 | "yarn-error.log*", 61 | "lerna-debug.log*", 62 | ".pnpm-debug.log*", 63 | "report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json", 64 | "pids", 65 | "*.pid", 66 | "*.seed", 67 | "*.pid.lock", 68 | "lib-cov", 69 | "coverage", 70 | ".nyc_output", 71 | "grunt-ts", 72 | "node_modules", 73 | "jspm_packages", 74 | "web_modules", 75 | "*.tsbuildinfo", 76 | "*.pyc", 77 | "__pycache__", 78 | "*.so", 79 | "*.egg", 80 | "*.egg-info", 81 | "dist", 82 | "build", 83 | "eggs", 84 | "parts", 85 | "bin", 86 | "var", 87 | "sdist", 88 | "develop-eggs", 89 | ".installed.cfg", 90 | "lib", 91 | "lib64", 92 | "__pycache__", 93 | "*.py[cod]", 94 | "*$py.class", 95 | "*.so", 96 | ".Python", 97 | "build", 98 | "develop-eggs", 99 | "dist", 100 | "downloads", 101 | "eggs", 102 | ".eggs", 103 | "lib", 104 | "lib64", 105 | "parts", 106 | "sdist", 107 | "var", 108 | "wheels", 109 | "share/python-wheels", 110 | "*.egg-info/", 111 | ".installed.cfg", 112 | "*.egg", 113 | "MANIFEST", 114 | "*.manifest", 115 | "*.spec", 116 | "pip-log.txt", 117 | "pip-delete-this-directory.txt", 118 | ".tox/", 119 | ".nox/", 120 | ".coverage", 121 | ".coverage.*", 122 | ".cache", 123 | "nosetests.xml", 124 | "coverage.xml", 125 | "*.cover", 126 | "*.py,cover", 127 | ".hypothesis/", 128 | ".pytest_cache/", 129 | "cover/", 130 | "*.mo", 131 | "*.pot", 132 | "*.log", 133 | "local_settings.py", 134 | "db.sqlite3", 135 | "db.sqlite3-journal", 136 | "instance/", 137 | ".webassets-cache", 138 | ".env", 139 | ".venv", 140 | "env/", 141 | "venv/", 142 | "ENV/", 143 | "env.bak/", 144 | "venv.bak/", 145 | ".spyderproject", 146 | ".spyproject", 147 | "*.swp", 148 | "*.swo", 149 | "*~", 150 | "*.orig", 151 | "*.rej", 152 | "*.bak", 153 | "*.tmp", 154 | "*.temp", 155 | ".DS_Store", 156 | ".DS_Store?", 157 | "_*", 158 | ".Spotlight-V100", 159 | ".Trashes", 160 | "ehthumbs.db", 161 | "Thumbs.db", 162 | "*.lnk", 163 | "*.tmp", 164 | "*.temp", 165 | "*.log", 166 | "*.pid", 167 | "*.seed", 168 | "*.pid.lock" 169 | ], 170 | "ignore_files": [ 171 | ".DS_Store", 172 | ".gitignore", 173 | ".gitattributes", 174 | ".editorconfig", 175 | "Thumbs.db", 176 | "desktop.ini", 177 | ".env", 178 | ".env.local", 179 | ".env.production", 180 | ".env.development", 181 | ".env.test", 182 | "*.log", 183 | "*.pid", 184 | "*.seed", 185 | "*.pid.lock", 186 | "*.swp", 187 | "*.swo", 188 | "*~", 189 | "*.orig", 190 | "*.rej", 191 | "*.bak", 192 | "*.tmp", 193 | "*.temp", 194 | "npm-debug.log*", 195 | "yarn-debug.log*", 196 | "yarn-error.log*", 197 | "lerna-debug.log*", 198 | ".pnpm-debug.log*", 199 | "report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json", 200 | "*.tsbuildinfo", 201 | "*.pyc", 202 | "__pycache__", 203 | "*.so", 204 | "*.egg", 205 | "*.egg-info", 206 | "dist", 207 | "build", 208 | "eggs", 209 | "parts", 210 | "bin", 211 | "var", 212 | "sdist", 213 | "develop-eggs", 214 | ".installed.cfg", 215 | "lib", 216 | "lib64", 217 | "__pycache__", 218 | "*.py[cod]", 219 | "*$py.class", 220 | "*.so", 221 | ".Python", 222 | "build", 223 | "develop-eggs", 224 | "dist", 225 | "downloads", 226 | "eggs", 227 | ".eggs", 228 | "lib", 229 | "lib64", 230 | "parts", 231 | "sdist", 232 | "var", 233 | "wheels", 234 | "share/python-wheels", 235 | "*.egg-info/", 236 | ".installed.cfg", 237 | "*.egg", 238 | "MANIFEST", 239 | "*.manifest", 240 | "*.spec", 241 | "pip-log.txt", 242 | "pip-delete-this-directory.txt", 243 | ".tox/", 244 | ".nox/", 245 | ".coverage", 246 | ".coverage.*", 247 | ".cache", 248 | "nosetests.xml", 249 | "coverage.xml", 250 | "*.cover", 251 | "*.py,cover", 252 | ".hypothesis/", 253 | ".pytest_cache/", 254 | "cover/", 255 | "*.mo", 256 | "*.pot", 257 | "*.log", 258 | "local_settings.py", 259 | "db.sqlite3", 260 | "db.sqlite3-journal", 261 | "instance/", 262 | ".webassets-cache", 263 | ".env", 264 | ".venv", 265 | "env/", 266 | "venv/", 267 | "ENV/", 268 | "env.bak/", 269 | "venv.bak/", 270 | ".spyderproject", 271 | ".spyproject", 272 | "*.swp", 273 | "*.swo", 274 | "*~", 275 | "*.orig", 276 | "*.rej", 277 | "*.bak", 278 | "*.tmp", 279 | "*.temp", 280 | ".DS_Store", 281 | ".DS_Store?", 282 | "_*", 283 | ".Spotlight-V100", 284 | ".Trashes", 285 | "ehthumbs.db", 286 | "Thumbs.db", 287 | "*.lnk", 288 | "*.tmp", 289 | "*.temp", 290 | "*.log", 291 | "*.pid", 292 | "*.seed", 293 | "*.pid.lock" 294 | ] 295 | } -------------------------------------------------------------------------------- /tests/test_core_threading.py: -------------------------------------------------------------------------------- 1 | # tests/test_core_threading.py 2 | # 测试多线程脚本描述生成功能 3 | 4 | import pytest 5 | import os 6 | import tempfile 7 | import time 8 | from unittest.mock import MagicMock, patch 9 | from pathlib import Path 10 | import sys 11 | 12 | # 添加项目根目录到路径 13 | root_dir = Path(__file__).parent.parent 14 | sys.path.append(str(root_dir)) 15 | 16 | from src.readmex.core import readmex 17 | 18 | 19 | class TestCoreThreading: 20 | """测试 readmex 的多线程功能""" 21 | 22 | def test_multithreaded_script_descriptions(self): 23 | """测试多线程脚本描述生成""" 24 | print("\n" + "=" * 60) 25 | print("🧵 测试: 多线程脚本描述生成") 26 | print("=" * 60) 27 | 28 | with tempfile.TemporaryDirectory() as temp_dir: 29 | # 创建测试文件 30 | test_files = { 31 | "app.py": "#!/usr/bin/env python\n# Main application file\nprint('Hello World')", 32 | "utils.py": "# Utility functions\ndef helper():\n return True", 33 | "config.py": "# Configuration settings\nDEBUG = True\nAPI_KEY = 'test'", 34 | "models.py": "# Data models\nclass User:\n def __init__(self, name):\n self.name = name" 35 | } 36 | 37 | # 写入测试文件 38 | for filename, content in test_files.items(): 39 | filepath = os.path.join(temp_dir, filename) 40 | with open(filepath, 'w', encoding='utf-8') as f: 41 | f.write(content) 42 | 43 | # 创建 readmex 实例并模拟 model_client 44 | craft = readmex(project_dir=temp_dir) 45 | 46 | # Mock model_client.get_answer 方法 47 | def mock_get_answer(prompt): 48 | # 模拟API调用延迟 49 | time.sleep(0.1) 50 | if "app.py" in prompt: 51 | return "主应用程序文件,包含应用启动逻辑" 52 | elif "utils.py" in prompt: 53 | return "工具函数模块,提供辅助功能" 54 | elif "config.py" in prompt: 55 | return "配置管理模块,定义应用配置" 56 | elif "models.py" in prompt: 57 | return "数据模型定义,包含用户类" 58 | else: 59 | return "Python脚本文件" 60 | 61 | craft.model_client.get_answer = mock_get_answer 62 | 63 | print(f"测试目录: {temp_dir}") 64 | print(f"测试文件数量: {len(test_files)}") 65 | 66 | # 测试单线程处理时间 67 | start_time = time.time() 68 | descriptions_single = craft._generate_script_descriptions(max_workers=1) 69 | single_thread_time = time.time() - start_time 70 | 71 | print(f"单线程处理时间: {single_thread_time:.2f} 秒") 72 | 73 | # 测试多线程处理时间 74 | start_time = time.time() 75 | descriptions_multi = craft._generate_script_descriptions(max_workers=3) 76 | multi_thread_time = time.time() - start_time 77 | 78 | print(f"多线程处理时间: {multi_thread_time:.2f} 秒") 79 | print(f"速度提升: {single_thread_time/multi_thread_time:.2f}x") 80 | 81 | # 验证结果 82 | import json 83 | desc_single = json.loads(descriptions_single) 84 | desc_multi = json.loads(descriptions_multi) 85 | 86 | assert len(desc_single) == len(test_files), f"单线程处理文件数不匹配: {len(desc_single)} vs {len(test_files)}" 87 | assert len(desc_multi) == len(test_files), f"多线程处理文件数不匹配: {len(desc_multi)} vs {len(test_files)}" 88 | 89 | # 验证描述内容 90 | for filename in test_files.keys(): 91 | assert filename in desc_single, f"单线程结果缺少文件: {filename}" 92 | assert filename in desc_multi, f"多线程结果缺少文件: {filename}" 93 | assert len(desc_single[filename]) > 0, f"单线程描述为空: {filename}" 94 | assert len(desc_multi[filename]) > 0, f"多线程描述为空: {filename}" 95 | 96 | print("✅ 多线程功能测试通过!") 97 | print(f" ✓ 处理文件数: {len(desc_multi)}") 98 | print(f" ✓ 性能提升: {single_thread_time/multi_thread_time:.2f}x") 99 | 100 | # 验证多线程确实比单线程快(考虑到测试环境的误差) 101 | if multi_thread_time < single_thread_time * 0.8: # 至少快20% 102 | print(" ✓ 多线程性能优化有效") 103 | else: 104 | print(" ⚠️ 多线程性能提升不明显(可能由于测试环境限制)") 105 | 106 | def test_empty_file_list(self): 107 | """测试空文件列表的处理""" 108 | print("\n" + "=" * 60) 109 | print("📂 测试: 空文件列表处理") 110 | print("=" * 60) 111 | 112 | with tempfile.TemporaryDirectory() as temp_dir: 113 | # 创建一个只有非脚本文件的目录 114 | with open(os.path.join(temp_dir, "README.md"), 'w') as f: 115 | f.write("# Test Project") 116 | 117 | craft = readmex(project_dir=temp_dir) 118 | 119 | descriptions = craft._generate_script_descriptions(max_workers=3) 120 | 121 | import json 122 | desc_dict = json.loads(descriptions) 123 | 124 | assert len(desc_dict) == 0, "空目录应该返回空字典" 125 | print("✅ 空文件列表处理测试通过!") 126 | 127 | def test_error_handling(self): 128 | """测试错误处理""" 129 | print("\n" + "=" * 60) 130 | print("🚨 测试: 错误处理机制") 131 | print("=" * 60) 132 | 133 | with tempfile.TemporaryDirectory() as temp_dir: 134 | # 创建测试文件 135 | test_file = os.path.join(temp_dir, "test.py") 136 | with open(test_file, 'w', encoding='utf-8') as f: 137 | f.write("print('test')") 138 | 139 | craft = readmex(project_dir=temp_dir) 140 | 141 | # Mock model_client.get_answer 抛出异常 142 | def mock_get_answer_error(prompt): 143 | raise Exception("API调用失败") 144 | 145 | craft.model_client.get_answer = mock_get_answer_error 146 | 147 | # 应该不会崩溃,而是优雅地处理错误 148 | descriptions = craft._generate_script_descriptions(max_workers=2) 149 | 150 | import json 151 | desc_dict = json.loads(descriptions) 152 | 153 | # 由于错误,可能没有成功处理任何文件 154 | print(f"错误情况下处理的文件数: {len(desc_dict)}") 155 | print("✅ 错误处理测试通过!") 156 | 157 | 158 | if __name__ == "__main__": 159 | pytest.main([__file__, "-v", "-s"]) -------------------------------------------------------------------------------- /tests/test_extended_auto_generation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify the extended auto-generation functionality for all fields: 4 | - Entry File 5 | - Key Features 6 | - Additional Information 7 | """ 8 | 9 | import sys 10 | import os 11 | 12 | def test_new_methods_exist(): 13 | """Test if all new auto-generation methods exist in the code""" 14 | print("=== Testing Extended Auto-Generation Methods ===") 15 | 16 | try: 17 | # Read the core.py file to check for method definitions 18 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py') 19 | 20 | with open(core_file_path, 'r', encoding='utf-8') as f: 21 | content = f.read() 22 | 23 | # Check if all new methods exist 24 | methods_to_check = [ 25 | 'def _generate_entry_file(self, structure, dependencies, descriptions):', 26 | 'def _generate_key_features(self, structure, dependencies, descriptions):', 27 | 'def _generate_additional_info(self, structure, dependencies, descriptions):' 28 | ] 29 | 30 | all_found = True 31 | for method_def in methods_to_check: 32 | if method_def in content: 33 | method_name = method_def.split('(')[0].replace('def ', '') 34 | print(f"✅ {method_name} method exists with correct signature") 35 | else: 36 | method_name = method_def.split('(')[0].replace('def ', '') 37 | print(f"❌ {method_name} method does not exist or has incorrect signature") 38 | all_found = False 39 | 40 | return all_found 41 | 42 | except Exception as e: 43 | print(f"❌ Error reading core.py: {e}") 44 | return False 45 | 46 | def test_generate_method_integration(): 47 | """Test if the generate method includes all auto-generation calls""" 48 | print("\n=== Testing Generate Method Integration ===") 49 | 50 | try: 51 | # Read the core.py file to check for auto-generation calls 52 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py') 53 | 54 | with open(core_file_path, 'r', encoding='utf-8') as f: 55 | content = f.read() 56 | 57 | # Check for all auto-generation calls in generate method 58 | auto_gen_calls = [ 59 | 'self._generate_entry_file(structure, dependencies, descriptions)', 60 | 'self._generate_key_features(structure, dependencies, descriptions)', 61 | 'self._generate_additional_info(structure, dependencies, descriptions)' 62 | ] 63 | 64 | all_found = True 65 | for call in auto_gen_calls: 66 | if call in content: 67 | print(f"✅ Found auto-generation call: {call}") 68 | else: 69 | print(f"❌ Missing auto-generation call: {call}") 70 | all_found = False 71 | 72 | # Check for conditional logic 73 | conditional_checks = [ 74 | 'if not self.config["entry_file"]:', 75 | 'if not self.config["key_features"]:', 76 | 'if not self.config["additional_info"]:' 77 | ] 78 | 79 | for check in conditional_checks: 80 | if check in content: 81 | print(f"✅ Found conditional check: {check}") 82 | else: 83 | print(f"❌ Missing conditional check: {check}") 84 | all_found = False 85 | 86 | return all_found 87 | 88 | except Exception as e: 89 | print(f"❌ Error reading core.py: {e}") 90 | return False 91 | 92 | def test_user_interface_updates(): 93 | """Test if user interface prompts have been updated""" 94 | print("\n=== Testing User Interface Updates ===") 95 | 96 | try: 97 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py') 98 | 99 | with open(core_file_path, 'r', encoding='utf-8') as f: 100 | content = f.read() 101 | 102 | # Check for updated prompts in both _get_basic_info and _get_project_meta_info 103 | expected_prompts = [ 104 | 'press Enter to auto-detect', # Entry file 105 | 'press Enter to auto-generate', # Key features and additional info 106 | 'will auto-detect based on project analysis', 107 | 'will auto-generate based on project analysis' 108 | ] 109 | 110 | all_found = True 111 | for prompt in expected_prompts: 112 | if prompt in content: 113 | print(f"✅ Found updated prompt: '{prompt}'") 114 | else: 115 | print(f"❌ Missing updated prompt: '{prompt}'") 116 | all_found = False 117 | 118 | return all_found 119 | 120 | except Exception as e: 121 | print(f"❌ Error checking user interface updates: {e}") 122 | return False 123 | 124 | def test_method_implementations(): 125 | """Test if the method implementations contain proper logic""" 126 | print("\n=== Testing Method Implementations ===") 127 | 128 | try: 129 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py') 130 | 131 | with open(core_file_path, 'r', encoding='utf-8') as f: 132 | content = f.read() 133 | 134 | # Check for key implementation details 135 | implementation_checks = [ 136 | 'Auto-detecting entry file based on project analysis', 137 | 'Auto-generating key features based on project analysis', 138 | 'Auto-generating additional information based on project analysis', 139 | 'self.model_client.get_answer(prompt)', 140 | 'detected_entry = detected_entry.strip()', 141 | 'generated_features = generated_features.strip()', 142 | 'generated_info = generated_info.strip()' 143 | ] 144 | 145 | all_found = True 146 | for check in implementation_checks: 147 | if check in content: 148 | print(f"✅ Found implementation detail: '{check}'") 149 | else: 150 | print(f"❌ Missing implementation detail: '{check}'") 151 | all_found = False 152 | 153 | return all_found 154 | 155 | except Exception as e: 156 | print(f"❌ Error checking method implementations: {e}") 157 | return False 158 | 159 | def main(): 160 | """Run all tests""" 161 | print("Testing Extended Auto-Generation Functionality\n") 162 | 163 | tests = [ 164 | test_new_methods_exist, 165 | test_generate_method_integration, 166 | test_user_interface_updates, 167 | test_method_implementations 168 | ] 169 | 170 | results = [] 171 | for test in tests: 172 | try: 173 | result = test() 174 | results.append(result) 175 | except Exception as e: 176 | print(f"❌ Test failed with exception: {e}") 177 | results.append(False) 178 | 179 | # Summary 180 | print("\n" + "="*50) 181 | print("EXTENDED AUTO-GENERATION TEST SUMMARY") 182 | print("="*50) 183 | 184 | passed = sum(results) 185 | total = len(results) 186 | 187 | if passed == total: 188 | print(f"🎉 All tests passed! ({passed}/{total})") 189 | print("✅ Extended auto-generation functionality is properly implemented") 190 | print("\n📋 Implementation Summary:") 191 | print(" • Entry file auto-detection") 192 | print(" • Key features auto-generation") 193 | print(" • Additional info auto-generation") 194 | print(" • User interface prompts updated") 195 | print(" • Generate method integration complete") 196 | else: 197 | print(f"⚠️ Some tests failed ({passed}/{total})") 198 | print("❌ Extended auto-generation functionality needs attention") 199 | 200 | return passed == total 201 | 202 | if __name__ == "__main__": 203 | success = main() 204 | sys.exit(0 if success else 1) -------------------------------------------------------------------------------- /tests/test_logo_generator.py: -------------------------------------------------------------------------------- 1 | # tests/test_logo_generator.py 2 | # 测试 logo_generator 的 generate_logo 函数 3 | 4 | import pytest 5 | import os 6 | import tempfile 7 | from unittest.mock import MagicMock, patch 8 | from rich.console import Console 9 | from pathlib import Path 10 | import sys 11 | 12 | root_dir = Path(__file__).parent.parent 13 | sys.path.append(str(root_dir)) 14 | from src.readmex.utils.logo_generator import generate_logo 15 | from src.readmex.utils.model_client import ModelClient 16 | 17 | 18 | class TestLogoGenerator: 19 | """测试 Logo 生成器""" 20 | 21 | def test_generate_logo_mock(self): 22 | """使用 Mock 测试 logo 生成逻辑""" 23 | print("\n" + "=" * 60) 24 | print("🧪 测试 1: Mock Logo 生成逻辑测试") 25 | print("=" * 60) 26 | 27 | # 创建临时目录 28 | with tempfile.TemporaryDirectory() as temp_dir: 29 | # 准备测试数据 30 | descriptions = """ 31 | { 32 | "main.py": "这是一个自动生成README文档的工具,使用AI技术分析项目结构和代码", 33 | "config.py": "配置文件管理模块,处理API密钥和模型设置" 34 | } 35 | """ 36 | 37 | # 创建 Mock 对象 38 | mock_model_client = MagicMock() 39 | console = Console() 40 | 41 | # 模拟 LLM 返回的logo描述 42 | mock_model_client.get_answer.return_value = ( 43 | "A modern AI-powered documentation tool logo with blue and green colors" 44 | ) 45 | 46 | # 模拟图片生成结果 47 | mock_image_result = { 48 | "url": "https://example.com/logo.png", 49 | "content": b"fake_png_content_123", 50 | } 51 | mock_model_client.get_image.return_value = mock_image_result 52 | 53 | # 调用函数 54 | logo_path = generate_logo( 55 | temp_dir, descriptions, mock_model_client, console 56 | ) 57 | 58 | # 验证结果 59 | assert logo_path is not None 60 | assert logo_path.endswith("logo.png") 61 | assert os.path.exists(logo_path) 62 | 63 | # 验证文件内容 64 | with open(logo_path, "rb") as f: 65 | content = f.read() 66 | assert content == b"fake_png_content_123" 67 | 68 | # 验证调用次数 69 | assert mock_model_client.get_answer.call_count == 1 70 | assert mock_model_client.get_image.call_count == 1 71 | 72 | def test_generate_logo_error_handling(self): 73 | """测试错误处理""" 74 | print("\n" + "=" * 60) 75 | print("🚨 测试 2: 错误处理测试") 76 | print("=" * 60) 77 | 78 | with tempfile.TemporaryDirectory() as temp_dir: 79 | descriptions = "测试项目" 80 | 81 | # 创建 Mock 对象,模拟图片生成失败 82 | mock_model_client = MagicMock() 83 | console = Console() 84 | 85 | # 模拟正常的描述生成 86 | mock_model_client.get_answer.return_value = "A test logo" 87 | 88 | # 模拟图片生成失败 89 | mock_image_result = {"error": "API调用失败"} 90 | mock_model_client.get_image.return_value = mock_image_result 91 | 92 | # 调用函数 93 | logo_path = generate_logo( 94 | temp_dir, descriptions, mock_model_client, console 95 | ) 96 | 97 | # 验证返回None 98 | assert logo_path is None 99 | 100 | def test_generate_logo_empty_content(self): 101 | """测试图片内容为空的情况""" 102 | print("\n" + "=" * 60) 103 | print("📭 测试 3: 空内容处理测试") 104 | print("=" * 60) 105 | 106 | with tempfile.TemporaryDirectory() as temp_dir: 107 | descriptions = "测试项目" 108 | 109 | mock_model_client = MagicMock() 110 | console = Console() 111 | 112 | # 模拟正常的描述生成 113 | mock_model_client.get_answer.return_value = "A test logo" 114 | 115 | # 模拟图片生成成功但内容为空 116 | mock_image_result = {"url": "https://example.com/logo.png", "content": None} 117 | mock_model_client.get_image.return_value = mock_image_result 118 | 119 | # 调用函数 120 | logo_path = generate_logo( 121 | temp_dir, descriptions, mock_model_client, console 122 | ) 123 | 124 | # 验证返回None 125 | assert logo_path is None 126 | 127 | def test_generate_logo_real_api(self): 128 | """使用真实API测试logo生成""" 129 | print("\n" + "=" * 60) 130 | print("🌐 测试 4: 真实 API Logo 生成测试") 131 | print("=" * 60) 132 | 133 | with tempfile.TemporaryDirectory() as temp_dir: 134 | descriptions = """ 135 | { 136 | "readmex/core.py": "readmex核心类,负责协调整个README生成流程", 137 | "readmex/utils/model_client.py": "模型客户端,支持LLM问答和AI文生图功能", 138 | "readmex/utils/logo_generator.py": "Logo生成器,根据项目描述生成专业的项目Logo" 139 | } 140 | """ 141 | 142 | # 使用真实的 ModelClient 143 | try: 144 | model_client = ModelClient() 145 | console = Console() 146 | 147 | print(f"测试目录: {temp_dir}") 148 | print("开始测试真实API logo生成...") 149 | 150 | logo_path = generate_logo(temp_dir, descriptions, model_client, console) 151 | 152 | # 如果遇到网络问题(SSL错误等),允许测试跳过 153 | if logo_path is None: 154 | pytest.skip("Logo生成失败,可能由于网络连接问题(SSL错误等)") 155 | 156 | # 验证生成结果 157 | assert os.path.exists(logo_path), f"Logo文件不存在: {logo_path}" 158 | 159 | # 检查文件大小 160 | file_size = os.path.getsize(logo_path) 161 | assert file_size > 0, "Logo文件为空" 162 | assert file_size > 1000, f"Logo文件太小,可能生成失败: {file_size} 字节" 163 | 164 | print(f"✅ Logo 生成成功!") 165 | print(f" 文件路径: {logo_path}") 166 | print(f" 文件大小: {file_size:,} 字节") 167 | 168 | # 验证文件是有效的图片格式 169 | with open(logo_path, "rb") as f: 170 | header = f.read(12) 171 | 172 | # 检查常见图片格式 173 | if header.startswith(b"\x89PNG\r\n\x1a\n"): 174 | image_format = "PNG" 175 | elif header.startswith(b"RIFF") and b"WEBP" in header: 176 | image_format = "WebP" 177 | elif header.startswith(b"\xff\xd8\xff"): 178 | image_format = "JPEG" 179 | else: 180 | # 打印文件头以便调试 181 | print(f" 文件头: {header}") 182 | image_format = "Unknown" 183 | # 不直接断言失败,而是警告 184 | print(f" ⚠️ 未知图片格式,但文件大小正常: {file_size:,} 字节") 185 | 186 | if image_format != "Unknown": 187 | print(f" 文件格式: {image_format} ✅") 188 | else: 189 | print(f" 文件格式: 未知但可接受 ⚠️") 190 | 191 | # 验证images目录结构 192 | images_dir = os.path.dirname(logo_path) 193 | assert ( 194 | os.path.basename(images_dir) == "images" 195 | ), "Logo应该保存在images目录中" 196 | 197 | print(f" 目录结构: 正确 ✅") 198 | print("🎉 真实API logo生成测试通过!") 199 | 200 | except Exception as e: 201 | # 如果是网络相关错误,跳过测试而不是失败 202 | if ( 203 | "SSL" in str(e) 204 | or "ConnectionError" in str(e) 205 | or "TimeoutError" in str(e) 206 | ): 207 | pytest.skip(f"网络连接问题,跳过测试: {e}") 208 | else: 209 | pytest.fail(f"真实API测试失败: {e}") 210 | 211 | def test_logo_description_generation(self): 212 | """测试Logo描述生成功能(仅测试LLM部分)""" 213 | print("\n" + "=" * 60) 214 | print("💬 测试 5: LLM Logo 描述生成测试") 215 | print("=" * 60) 216 | 217 | try: 218 | model_client = ModelClient() 219 | console = Console() 220 | 221 | descriptions = """ 222 | { 223 | "app.py": "一个简单的Web应用", 224 | "models.py": "数据库模型定义", 225 | "utils.py": "工具函数集合" 226 | } 227 | """ 228 | 229 | print("测试Logo描述生成...") 230 | 231 | # 调用 LLM 生成 logo 描述 232 | prompt = f"""基于以下项目文件描述,为这个项目设计一个专业的logo描述。 233 | 234 | 项目文件描述: 235 | {descriptions} 236 | 237 | 请用英文生成一个详细的logo设计描述,包括: 238 | 1. 视觉元素和符号 239 | 2. 颜色方案 240 | 3. 整体风格 241 | 4. 技术感和专业性 242 | 243 | 描述应该适合用于AI图像生成,清晰明确。""" 244 | 245 | logo_description = model_client.get_answer(prompt) 246 | 247 | # 验证返回结果 248 | assert logo_description is not None, "Logo描述生成失败" 249 | assert len(logo_description.strip()) > 20, "Logo描述太短" 250 | assert isinstance(logo_description, str), "Logo描述应该是字符串" 251 | 252 | print(f"✅ Logo描述生成成功!") 253 | print(f" 描述长度: {len(logo_description)} 字符") 254 | print(f" 描述内容: {logo_description}") 255 | 256 | # 检查描述是否包含一些常见的设计元素词汇 257 | design_keywords = [ 258 | "logo", 259 | "color", 260 | "design", 261 | "professional", 262 | "modern", 263 | "symbol", 264 | "icon", 265 | ] 266 | found_keywords = [ 267 | word 268 | for word in design_keywords 269 | if word.lower() in logo_description.lower() 270 | ] 271 | 272 | print(f" 包含设计关键词: {', '.join(found_keywords)}") 273 | assert ( 274 | len(found_keywords) >= 2 275 | ), f"Logo描述应该包含更多设计相关词汇,当前只有: {found_keywords}" 276 | 277 | print("🎉 Logo描述生成测试通过!") 278 | 279 | except Exception as e: 280 | if "SSL" in str(e) or "ConnectionError" in str(e): 281 | pytest.skip(f"网络连接问题,跳过描述生成测试: {e}") 282 | else: 283 | pytest.fail(f"Logo描述生成测试失败: {e}") 284 | 285 | 286 | if __name__ == "__main__": 287 | pytest.main([__file__, "-v", "-s"]) 288 | -------------------------------------------------------------------------------- /tests/test_complete_dependency_analysis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 完整的多语言依赖分析测试 4 | 测试项目根目录,语言为 Python 5 | """ 6 | 7 | import os 8 | import sys 9 | import tempfile 10 | from pathlib import Path 11 | 12 | # 添加项目根目录到 Python 路径 13 | project_root = Path(__file__).parent.parent # tests 目录的父目录 14 | sys.path.insert(0, str(project_root / "src")) 15 | 16 | from readmex.utils.dependency_analyzer import DependencyAnalyzer 17 | from readmex.utils.model_client import ModelClient 18 | 19 | 20 | def test_complete_dependency_analysis(): 21 | """对项目根目录进行完整的依赖分析测试""" 22 | print("=" * 70) 23 | print("完整的多语言依赖分析测试") 24 | print("测试目录: 项目根目录") 25 | print("测试语言: Python") 26 | print("=" * 70) 27 | 28 | # 设置测试目录 - 测试项目根目录 29 | test_project_dir = project_root 30 | 31 | if not test_project_dir.exists(): 32 | print(f"❌ 测试目录不存在: {test_project_dir}") 33 | return 34 | 35 | print(f"📁 测试目录: {test_project_dir}") 36 | print() 37 | 38 | try: 39 | # 1. 创建依赖分析器实例 40 | print("1️⃣ 创建 DependencyAnalyzer 实例") 41 | 42 | # 创建真实的 ModelClient 实例 43 | try: 44 | model_client = ModelClient() 45 | print("✅ ModelClient 创建成功") 46 | except Exception as e: 47 | print(f"⚠️ ModelClient 创建失败: {e}") 48 | print(" 将使用 None 作为 model_client") 49 | model_client = None 50 | 51 | analyzer = DependencyAnalyzer( 52 | project_dir=str(test_project_dir), 53 | primary_language="python", 54 | model_client=model_client 55 | ) 56 | print(f"✅ 分析器创建成功") 57 | print(f" 当前语言: {analyzer.primary_language}") 58 | print(f" 项目目录: {analyzer.project_dir}") 59 | print(f" ModelClient 状态: {'✅ 可用' if model_client else '❌ 不可用'}") 60 | print() 61 | 62 | # 2. 测试支持的语言 63 | print("2️⃣ 测试支持的语言") 64 | supported_languages = analyzer.get_supported_languages() 65 | print(f"✅ 支持的语言 ({len(supported_languages)} 种):") 66 | for i, lang in enumerate(supported_languages, 1): 67 | print(f" {i:2d}. {lang}") 68 | print() 69 | 70 | # 3. 测试配置加载 71 | print("3️⃣ 测试配置加载") 72 | config = analyzer.config 73 | print(f"✅ 配置加载成功") 74 | print(f" 默认语言: {config['default_language']}") 75 | print(f" Python 配置:") 76 | python_config = config['languages']['python'] 77 | print(f" 依赖文件: {python_config['dependency_files']}") 78 | print(f" 文件扩展名: {python_config['file_extensions']}") 79 | print(f" 导入模式数量: {len(python_config['import_patterns'])}") 80 | print() 81 | 82 | # 4. 测试项目导入提取 83 | print("4️⃣ 测试项目导入提取") 84 | imports = analyzer.get_project_imports() 85 | print(f"✅ 发现 {len(imports)} 个导入语句") 86 | 87 | if imports: 88 | print(" 前15个导入语句:") 89 | for i, imp in enumerate(sorted(imports)[:15], 1): 90 | print(f" {i:2d}. {imp}") 91 | 92 | if len(imports) > 15: 93 | print(f" ... 还有 {len(imports) - 15} 个导入语句") 94 | else: 95 | print(" ⚠️ 未发现导入语句") 96 | print() 97 | 98 | # 5. 测试外部依赖过滤 99 | print("5️⃣ 测试外部依赖过滤") 100 | external_imports = analyzer._filter_external_imports(imports) 101 | print(f"✅ 发现 {len(external_imports)} 个外部依赖") 102 | 103 | if external_imports: 104 | print(" 外部依赖列表:") 105 | for i, imp in enumerate(sorted(external_imports), 1): 106 | print(f" {i:2d}. {imp}") 107 | else: 108 | print(" ⚠️ 未发现外部依赖") 109 | print() 110 | 111 | # 6. 测试内置模块过滤 112 | print("6️⃣ 测试内置模块过滤") 113 | builtin_modules = config.get("builtin_modules", {}).get("python", []) 114 | print(f"✅ Python 内置模块数量: {len(builtin_modules)}") 115 | print(" 前10个内置模块:") 116 | for i, module in enumerate(builtin_modules[:10], 1): 117 | print(f" {i:2d}. {module}") 118 | if len(builtin_modules) > 10: 119 | print(f" ... 还有 {len(builtin_modules) - 10} 个内置模块") 120 | print() 121 | 122 | # 7. 测试现有依赖文件检测 123 | print("7️⃣ 测试现有依赖文件检测") 124 | existing_deps = analyzer.get_existing_requirements() 125 | if existing_deps: 126 | print(f"✅ 发现现有依赖文件,内容长度: {len(existing_deps)} 字符") 127 | print(" 依赖文件内容预览:") 128 | preview = existing_deps[:300] 129 | print(f" {preview}") 130 | if len(existing_deps) > 300: 131 | print(" ...") 132 | else: 133 | print(" ℹ️ 未发现现有依赖文件") 134 | print() 135 | 136 | # 8. 测试语言切换功能 137 | print("8️⃣ 测试语言切换功能") 138 | original_lang = analyzer.primary_language 139 | 140 | # 测试切换到 JavaScript 141 | success = analyzer.set_language("javascript") 142 | print(f" 切换到 JavaScript: {'✅ 成功' if success else '❌ 失败'}") 143 | print(f" 当前语言: {analyzer.primary_language}") 144 | 145 | # 测试切换到不支持的语言 146 | success = analyzer.set_language("unsupported_language") 147 | print(f" 切换到不支持的语言: {'❌ 失败' if not success else '✅ 成功'} (预期失败)") 148 | print(f" 当前语言: {analyzer.primary_language}") 149 | 150 | # 切换回原语言 151 | analyzer.set_language(original_lang) 152 | print(f" 切换回 {original_lang}: ✅ 成功") 153 | print() 154 | 155 | # 9. 测试模块名提取 156 | print("9️⃣ 测试模块名提取") 157 | test_imports = [ 158 | "import requests", 159 | "from flask import Flask", 160 | "import numpy.array", 161 | "from datetime import datetime" 162 | ] 163 | 164 | print(" 测试模块名提取:") 165 | for imp in test_imports: 166 | module_name = analyzer._extract_module_name(imp) 167 | print(f" '{imp}' -> '{module_name}'") 168 | print() 169 | 170 | # 10. 测试依赖内容清理 171 | print("🔟 测试依赖内容清理") 172 | test_content = """``` 173 | Based on analysis: 174 | requests>=2.25.0 175 | flask>=2.0.0 176 | numpy 177 | ``` 178 | Some other text 179 | pandas>=1.3.0 180 | """ 181 | cleaned = analyzer._clean_dependency_content(test_content) 182 | print(" 原始内容:") 183 | print(f" {repr(test_content)}") 184 | print(" 清理后内容:") 185 | print(f" {repr(cleaned)}") 186 | print() 187 | 188 | # 11. 测试完整的依赖分析流程(包含 LLM 调用) 189 | print("1️⃣1️⃣ 测试完整的依赖分析流程") 190 | try: 191 | # 创建临时输出目录 192 | with tempfile.TemporaryDirectory() as temp_dir: 193 | print(f" 临时输出目录: {temp_dir}") 194 | 195 | if model_client is not None: 196 | print(" 🚀 开始完整的依赖分析(包含 LLM 调用)...") 197 | 198 | # 执行完整的依赖分析 199 | result = analyzer.analyze_project_dependencies(output_dir=temp_dir) 200 | 201 | if result: 202 | print(" ✅ 依赖分析完成!") 203 | print(" ✅ 项目文件扫描完成") 204 | print(" ✅ 导入语句提取完成") 205 | print(" ✅ 外部依赖过滤完成") 206 | print(" ✅ LLM 生成依赖文件完成") 207 | print(" ✅ 依赖文件保存完成") 208 | 209 | # 检查生成的文件 210 | output_path = Path(temp_dir) 211 | generated_files = list(output_path.glob("*")) 212 | print(f" 📁 生成的文件数量: {len(generated_files)}") 213 | 214 | for file in generated_files: 215 | print(f" - {file.name}") 216 | if file.suffix == '.txt' and file.stat().st_size < 1000: 217 | # 显示小文件内容 218 | content = file.read_text()[:200] 219 | print(f" 内容预览: {content}...") 220 | else: 221 | print(" ❌ 依赖分析失败") 222 | 223 | else: 224 | print(" ⚠️ 跳过 LLM 调用部分(model_client=None)") 225 | print(" 测试依赖分析流程的其他部分...") 226 | 227 | # 手动测试各个步骤 228 | print(" ✅ 项目文件扫描完成") 229 | print(" ✅ 导入语句提取完成") 230 | print(" ✅ 外部依赖过滤完成") 231 | print(" ⚠️ LLM 生成跳过(需要 API 密钥)") 232 | 233 | except Exception as e: 234 | print(f" ❌ 分析过程出错: {e}") 235 | import traceback 236 | traceback.print_exc() 237 | print() 238 | 239 | # 12. 生成测试报告 240 | print("1️⃣2️⃣ 测试报告总结") 241 | print("=" * 50) 242 | print(f"📊 测试统计:") 243 | print(f" • 测试目录: {test_project_dir}") 244 | print(f" • 主要语言: {analyzer.primary_language}") 245 | print(f" • 支持语言数: {len(supported_languages)}") 246 | print(f" • 发现导入数: {len(imports)}") 247 | print(f" • 外部依赖数: {len(external_imports)}") 248 | print(f" • 内置模块数: {len(builtin_modules)}") 249 | print(f" • 现有依赖文件: {'是' if existing_deps else '否'}") 250 | 251 | print(f"\n🎯 主要外部依赖:") 252 | if external_imports: 253 | # 提取主要的包名 254 | packages = set() 255 | for imp in external_imports: 256 | module_name = analyzer._extract_module_name(imp) 257 | packages.add(module_name) 258 | 259 | for i, pkg in enumerate(sorted(packages), 1): 260 | print(f" {i:2d}. {pkg}") 261 | else: 262 | print(" 无外部依赖") 263 | 264 | print(f"\n✅ 测试完成!DependencyAnalyzer 功能正常") 265 | 266 | except Exception as e: 267 | print(f"❌ 测试过程中出现错误: {e}") 268 | import traceback 269 | traceback.print_exc() 270 | 271 | print("\n" + "=" * 70) 272 | print("测试结束") 273 | print("=" * 70) 274 | 275 | 276 | if __name__ == "__main__": 277 | test_complete_dependency_analysis() -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | [English](README.md) | 简体中文 7 | 8 |
9 | 10 | 11 |
12 | Poster 13 |
14 | 15 | --- 16 | 17 | 18 |
19 |
20 | 23 | 24 |

readmex

25 | 26 |

27 | 🚀 AI智能README生成器:自动为任何仓库创建精美的README和交互式Wiki!可以完全本地化运行,使用你自己的模型。 28 |
29 | 探索文档 » 30 |
31 |

32 | 33 | 34 | [![贡献者][contributors-shield]][contributors-url] 35 | [![分支][forks-shield]][forks-url] 36 | [![星标][stars-shield]][stars-url] 37 | [![问题][issues-shield]][issues-url] 38 | 40 | [![许可证][license-shield]][license-url] 41 | 42 |

43 | 查看演示 44 | · 45 | 报告Bug 46 | · 47 | 请求功能 48 |

49 |
50 | 51 | 52 |
53 | 目录 54 |
    55 |
  1. 56 | 关于项目 57 | 60 |
  2. 61 |
  3. 62 | 快速开始 63 | 67 |
  4. 68 |
  5. 使用方法
  6. 69 |
  7. 路线图
  8. 70 |
  9. 贡献
  10. 71 |
  11. 许可证
  12. 72 |
  13. 联系方式
  14. 73 |
  15. 致谢
  16. 74 |
75 |
76 | 77 | 78 | ## 📖 关于项目 79 | 80 | [![流程图](images/flow.png)](https://example.com) 81 | 82 | AI智能README生成器是一个基于AI的工具,可以自动为您的项目生成全面的Markdown README文件。它能够生成结构良好的文档,包括项目详情、技术栈、设置说明、使用示例、徽章、Logo等。 83 | 84 | ### 核心功能 85 | 86 | - 🤖 **AI驱动的README生成**:即时生成全面的Markdown README文档 87 | - 🔗 **自动徽章生成**:创建并嵌入相关的状态徽章(贡献者、分支、星标等) 88 | - 🖼️ **智能Logo设计**:自动生成独特的项目Logo 89 | - 🧠 **技术栈识别**:自动检测并包含项目的技术栈 90 | - 🌐 **上下文感知智能**:根据项目的特定上下文和需求定制内容 91 | 92 |

(返回顶部)

93 | 94 | ### 技术栈 95 | 96 | - [![Python][Python]][Python-url] 97 | - [![OpenAI][OpenAI]][OpenAI-url] 98 | - [![Rich][Rich]][Rich-url] 99 | 100 | ### 支持的编程语言 101 | 102 |
103 | 点击展开支持的编程语言 104 | 105 | #### Web开发 106 | - **JavaScript** 107 | - **TypeScript** 108 | - **HTML** 109 | - **CSS** 110 | - **SCSS** 111 | - **Sass** 112 | - **Less** 113 | - **Stylus** 114 | - **Pug** 115 | - **Handlebars** 116 | - **Mustache** 117 | - **Twig** 118 | - **Smarty** 119 | - **Jinja** 120 | - **Vue** 121 | 122 | #### 编程语言 123 | - **Python** 124 | - **Java** 125 | - **C** 126 | - **C++** 127 | - **C#** 128 | - **Go** 129 | - **Rust** 130 | - **PHP** 131 | - **Ruby** 132 | - **Swift** 133 | - **Kotlin** 134 | - **Scala** 135 | - **R** 136 | - **MATLAB** 137 | - **Perl** 138 | - **Lua** 139 | - **Dart** 140 | - **F#** 141 | - **Visual Basic** 142 | - **Assembly** 143 | - **Objective-C** 144 | - **Haskell** 145 | - **Erlang** 146 | - **Elixir** 147 | - **Clojure** 148 | - **CoffeeScript** 149 | - **PowerShell** 150 | - **Shell** 151 | - **Batch** 152 | - **Solidity** 153 | 154 | #### 构建与配置 155 | - **Dockerfile** 156 | - **Makefile** 157 | - **CMake** 158 | - **Gradle** 159 | - **Maven** 160 | - **Nix** 161 | - **Terraform** 162 | 163 | #### 数据与文档 164 | - **Jupyter** 165 | - **Protobuf** 166 | - **GraphQL** 167 | - **WebAssembly** 168 | 169 | #### 编辑器与IDE 170 | - **Vim** 171 | - **Emacs** 172 | 173 |
174 | 175 |

(返回顶部)

176 | 177 | 178 | ## 🚀 快速开始 179 | 180 | 以下是在本地设置项目的示例说明。要获取本地副本并运行,请按照以下简单步骤操作。 181 | 182 | ### 前置要求 183 | 184 | - Python 3.7+ 185 | 186 | ### 安装 187 | 188 | 1. 使用 pip 安装软件包: 189 | ```bash 190 | pip install readmex 191 | ``` 192 | 193 | ### 配置 194 | 195 | `readmex` 需要语言模型(用于生成文本)和文生图模型(用于生成Logo)的API密钥。您可以通过以下两种方式之一进行配置。环境变量的优先级更高。个人信息也可以在全局文件中设置,以作为交互式会话期间的默认值。 196 | 197 | #### 1. 环境变量 (推荐在CI/CD环境中使用) 198 | 199 | 在您的 shell 中设置以下环境变量: 200 | 201 | ```bash 202 | export LLM_API_KEY="your_llm_api_key" # 必填 203 | export T2I_API_KEY="your_t2i_api_key" # 必填 204 | 205 | # 可选:指定自定义API端点和模型 206 | export LLM_BASE_URL="https://api.example.com/v1" 207 | export T2I_BASE_URL="https://api.example.com/v1" 208 | export LLM_MODEL_NAME="your-llm-model" 209 | export T2I_MODEL_NAME="your-t2i-model" 210 | 211 | # 可选:RAG(检索增强生成)的嵌入模型配置 212 | export EMBEDDING_API_KEY="your_embedding_api_key" # 可选,用于Web嵌入模型 213 | export EMBEDDING_BASE_URL="https://api.example.com/v1" # 可选,用于Web嵌入模型 214 | export EMBEDDING_MODEL_NAME="text-embedding-3-small" # 可选,嵌入模型名称 215 | export LOCAL_EMBEDDING="true" # 可选,使用本地嵌入模型(默认:true) 216 | 217 | # 可选:性能配置 218 | export MAX_WORKERS="10" # 可选,最大并发线程数(默认:10) 219 | ``` 220 | 221 | #### 2. 全局配置文件 (推荐在本地使用) 222 | 223 | 为了方便,您可以创建一个全局配置文件,工具会自动查找它。 224 | 225 | 1. 创建目录:`mkdir -p ~/.readmex` 226 | 2. 创建配置文件:`~/.readmex/config.json` 227 | 3. 添加您的凭据和任何可选设置: 228 | 229 | ```json 230 | { 231 | "LLM_API_KEY": "在此处填入您的LLM API密钥", 232 | "T2I_API_KEY": "在此处填入您的T2I API密钥", 233 | "LLM_BASE_URL": "https://api.example.com/v1", 234 | "T2I_BASE_URL": "https://api.example.com/v1", 235 | "LLM_MODEL_NAME": "your-llm-model", 236 | "T2I_MODEL_NAME": "your-t2i-model", 237 | "EMBEDDING_API_KEY": "您的嵌入模型API密钥", 238 | "EMBEDDING_BASE_URL": "https://api.example.com/v1", 239 | "EMBEDDING_MODEL_NAME": "text-embedding-3-small", 240 | "LOCAL_EMBEDDING": "true", 241 | "MAX_WORKERS": "10", 242 | "github_username": "您的GitHub用户名", 243 | "twitter_handle": "您的Twitter用户名", 244 | "linkedin_username": "您的LinkedIn用户名", 245 | "email": "您的电子邮箱" 246 | } 247 | ``` 248 | 249 |

(返回顶部)

250 | 251 | 252 | ## 💻 使用方法 253 | 254 | 有三种方式运行 `readmex` 工具: 255 | 256 | ### 方法 1:安装后直接使用(推荐) 257 | 258 | 安装完成后,您可以在命令行中直接使用 `readmex` 命令: 259 | ```bash 260 | readmex 261 | ``` 262 | 263 | ### 方法 2:作为 Python 模块运行 264 | 265 | 如果您没有安装包或想要使用开发版本: 266 | ```bash 267 | # 使用简化的模块调用 268 | python -m readmex 269 | 270 | # 或者使用完整的模块路径 271 | python -m readmex.utils.cli 272 | ``` 273 | 274 | ### 方法 3:开发者模式(直接运行脚本) 275 | 276 | 对于开发者或想要直接运行源代码: 277 | ```bash 278 | python src/readmex/utils/cli.py 279 | ``` 280 | 281 | ### 命令行选项 282 | 283 | 所有运行方式都支持以下选项: 284 | ```bash 285 | # 基本用法 286 | readmex 287 | 288 | # 指定项目路径和输出目录 289 | readmex --project-path /path/to/your/project --output-dir /path/to/output 290 | 291 | # 生成网站 292 | readmex --website 293 | 294 | # 启动本地服务器 295 | readmex --serve 296 | 297 | # 部署到 GitHub Pages 298 | readmex --deploy 299 | 300 | # 查看帮助 301 | readmex --help 302 | ``` 303 | 304 | 这将会: 305 | 1. 生成`project_structure.txt`文件,包含项目结构 306 | 2. 生成`script_description.json`文件,包含项目中脚本的描述 307 | 3. 生成`requirements.txt`文件,包含项目的依赖要求 308 | 4. 生成`logo.png`文件,包含项目的Logo 309 | 5. 生成`README.md`文件,包含项目的README文档 310 | 311 |

(返回顶部)

312 | 313 | 314 | ## 🗺️ 路线图 315 | 316 | - [ ] Logo生成的提示工程优化 317 | - [ ] 多语言支持 318 | - [ ] 增强AI对项目功能的描述能力 319 | 320 | 查看[开放问题](https://github.com/aibox22/readmex/issues)以获取提议功能(和已知问题)的完整列表。 321 | 322 |

(返回顶部)

323 | 324 | 325 | ## 🤝 贡献 326 | 327 | 贡献让开源社区成为了一个学习、启发和创造的绝佳场所。您所做的任何贡献都是**非常感谢**的。 328 | 329 | 如果您有建议可以改善此项目,请fork该仓库并创建一个pull request。您也可以简单地创建一个带有"enhancement"标签的issue。 330 | 不要忘记给项目点个星!再次感谢! 331 | 332 | 1. Fork此项目 333 | 2. 创建您的功能分支 (`git checkout -b feature/AmazingFeature`) 334 | 3. 提交您的更改 (`git commit -m 'Add some AmazingFeature'`) 335 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 336 | 5. 开启一个Pull Request 337 | 338 |

(返回顶部)

339 | 340 | ### 主要贡献者: 341 | 342 | 343 | contrib.rocks image 344 | 345 | 346 | 347 | ## 🎗 许可证 348 | 349 | 版权所有 © 2024-2025 [readmex][readmex]。
350 | 基于[MIT][license-url]许可证发布。 351 | 352 |

(返回顶部)

353 | 354 | 355 | ## 📧 联系方式 356 | 357 | 邮箱:lintaothu@foxmail.com 358 | 359 | 项目链接:[https://github.com/aibox22/readmex](https://github.com/aibox22/readmex) 360 | 361 | QQ群:2161023585(欢迎加入我们的QQ群进行讨论和获取帮助!) 362 | 363 |
364 | QQ群二维码 365 |

扫描二维码加入我们的QQ群

366 |
367 | 368 |

(返回顶部)

369 | 370 | 371 | [readmex]: https://github.com/aibox22/readmex 372 | 373 | 374 | [contributors-shield]: https://img.shields.io/github/contributors/aibox22/readmex.svg?style=flat-round 375 | [contributors-url]: https://github.com/aibox22/readmex/graphs/contributors 376 | [forks-shield]: https://img.shields.io/github/forks/aibox22/readmex.svg?style=flat-round 377 | [forks-url]: https://github.com/aibox22/readmex/network/members 378 | [stars-shield]: https://img.shields.io/github/stars/aibox22/readmex.svg?style=flat-round 379 | [stars-url]: https://github.com/aibox22/readmex/stargazers 380 | [issues-shield]: https://img.shields.io/github/issues/aibox22/readmex.svg?style=flat-round 381 | [issues-url]: https://github.com/aibox22/readmex/issues 382 | [release-shield]: https://img.shields.io/github/v/release/aibox22/readmex?style=flat-round 383 | [release-url]: https://github.com/aibox22/readmex/releases 384 | [release-date-shield]: https://img.shields.io/github/release-date/aibox22/readmex?color=9cf&style=flat-round 385 | [license-shield]: https://img.shields.io/github/license/aibox22/readmex.svg?style=flat-round 386 | [license-url]: https://github.com/aibox22/readmex/blob/master/LICENSE.txt 387 | [Python]: https://img.shields.io/badge/Python-3776AB?style=flat-round&logo=python&logoColor=white 388 | [Python-url]: https://www.python.org/ 389 | [OpenAI]: https://img.shields.io/badge/OpenAI-000000?style=flat-round&logo=openai&logoColor=white 390 | [OpenAI-url]: https://openai.com/ 391 | [Flask]: https://img.shields.io/badge/Flask-000000?style=flat-round&logo=flask&logoColor=white 392 | [Flask-url]: https://flask.palletsprojects.com/ 393 | [Rich]: https://img.shields.io/badge/Rich-000000?style=flat-round&logo=rich&logoColor=white 394 | [Rich-url]: https://rich.readthedocs.io/ 395 | 396 | 397 | ## ⭐ 星标历史 398 | 399 |
400 | 401 | Star History Chart 402 | 403 |
404 | -------------------------------------------------------------------------------- /src/readmex/templates/BLANK_README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --- 5 | 6 | 7 | 8 | 9 |
10 |
11 | 14 | 15 |

{{project_title}}

16 | 17 |

18 | {{project_description}} 19 |
20 | Explore the docs » 21 |
22 |

23 | 24 | 25 | [![Contributors][contributors-shield]][contributors-url] 26 | [![Forks][forks-shield]][forks-url] 27 | [![Stargazers][stars-shield]][stars-url] 28 | [![Issues][issues-shield]][issues-url] 29 | 31 | [![License][license-shield]][license-url] 32 | 33 |

34 | View Demo 35 | · 36 | Report Bug 37 | · 38 | Request Feature 39 |

40 |
41 | 42 | 43 | 44 | 45 |
46 | Table of Contents 47 |
    48 |
  1. 49 | About The Project 50 | 53 |
  2. 54 |
  3. 55 | Getting Started 56 | 60 |
  4. 61 |
  5. Usage
  6. 62 |
  7. Roadmap
  8. 63 |
  9. Contributing
  10. 64 |
  11. License
  12. 65 |
  13. Contact
  14. 66 |
  15. Acknowledgments
  16. 67 |
68 |
69 | 70 | 71 | 72 | 73 | ## 📖 About The Project 74 | 75 | [![Flow Chart](images/flow.png)](https://example.com) 76 | 77 | {{project_description}} 78 | 79 | ### Key Features 80 | 81 | {{key_features_section}} 82 | 83 |

(back to top)

84 | 85 | 86 | 87 | ### Built With 88 | 89 | {{built_with_section}} 90 | for example: 91 | * [![React][React.js]][React-url] 92 | * [![Vue][Vue.js]][Vue-url] 93 | 94 |

(back to top)

95 | 96 | 97 | 98 | ### 📁 Project Structure 99 | 100 |
101 | Click to expand project structure 102 | 103 | ``` 104 | {{project_structure}} 105 | ``` 106 | 107 |
108 | 109 |

(back to top)

110 | 111 | 112 | 113 | 114 | ## 🚀 Getting Started 115 | 116 | This is an example of how you may give instructions on setting up your project locally. To get a local copy up and running follow these simple steps. 117 | 118 | ### Prerequisites 119 | 120 | {{prerequisites_section}} 121 | 122 | ### Installation 123 | 124 | {{installation_commands}} 125 | 126 | ### Configuration 127 | 128 | {{configuration_section}} 129 | 130 |

(back to top)

131 | 132 | 133 | 134 | 135 | ## 💻 Usage 136 | 137 | {{usage_section}} 138 | 139 |

(back to top)

140 | 141 | 142 | 143 | 144 | ## 🗺️ Roadmap 145 | 146 | {{roadmap_section}} 147 | 148 | See the [open issues](https://github.com/{{github_username}}/{{repo_name}}/issues) for a full list of proposed features (and known issues). 149 | 150 |

(back to top)

151 | 152 | 153 | 154 | 155 | ## 🤝 Contributing 156 | 157 | Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 158 | 159 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 160 | Don't forget to give the project a star! Thanks again! 161 | 162 | 1. Fork the Project 163 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 164 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 165 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 166 | 5. Open a Pull Request 167 | 168 |

(back to top)

169 | 170 | ### Top contributors: 171 | 172 | 173 | contrib.rocks image 174 | 175 | 176 | 177 | 178 | 179 | ## 🎗 License 180 | 181 | Copyright © 2024-2025 [{{project_title}}][{{project_title}}].
182 | Released under the [{{project_license}}][license-url] license. 183 | 184 |

(back to top)

185 | 186 | 187 | 188 | 189 | ## 📧 Contact 190 | 191 | Email: {{email}} 192 | 193 | Project Link: [https://github.com/{{github_username}}/{{repo_name}}](https://github.com/{{github_username}}/{{repo_name}}) 194 | 195 | {{contact_additional_info}} 196 | 197 |

(back to top)

198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | [{{project_title}}]: https://github.com/{{github_username}}/{{repo_name}} 207 | 208 | 209 | [contributors-shield]: https://img.shields.io/github/contributors/{{github_username}}/{{repo_name}}.svg?style=flat-round 210 | [contributors-url]: https://github.com/{{github_username}}/{{repo_name}}/graphs/contributors 211 | [forks-shield]: https://img.shields.io/github/forks/{{github_username}}/{{repo_name}}.svg?style=flat-round 212 | [forks-url]: https://github.com/{{github_username}}/{{repo_name}}/network/members 213 | [stars-shield]: https://img.shields.io/github/stars/{{github_username}}/{{repo_name}}.svg?style=flat-round 214 | [stars-url]: https://github.com/{{github_username}}/{{repo_name}}/stargazers 215 | [issues-shield]: https://img.shields.io/github/issues/{{github_username}}/{{repo_name}}.svg?style=flat-round 216 | [issues-url]: https://github.com/{{github_username}}/{{repo_name}}/issues 217 | [release-shield]: https://img.shields.io/github/v/release/{{github_username}}/{{repo_name}}?style=flat-round 218 | [release-url]: https://github.com/{{github_username}}/{{repo_name}}/releases 219 | [release-date-shield]: https://img.shields.io/github/release-date/{{github_username}}/{{repo_name}}?color=9cf&style=flat-round 220 | [license-shield]: https://img.shields.io/github/license/{{github_username}}/{{repo_name}}.svg?style=flat-round 221 | [license-url]: https://github.com/{{github_username}}/{{repo_name}}/blob/master/LICENSE.txt 222 | 223 | 224 | 225 | [Python]: https://img.shields.io/badge/Python-3776AB?style=flat-round&logo=python&logoColor=white 226 | [Python-url]: https://www.python.org/ 227 | [JavaScript]: https://img.shields.io/badge/JavaScript-F7DF1E?style=flat-round&logo=javascript&logoColor=black 228 | [JavaScript-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript 229 | [TypeScript]: https://img.shields.io/badge/TypeScript-007ACC?style=flat-round&logo=typescript&logoColor=white 230 | [TypeScript-url]: https://www.typescriptlang.org/ 231 | [Java]: https://img.shields.io/badge/Java-ED8B00?style=flat-round&logo=openjdk&logoColor=white 232 | [Java-url]: https://www.oracle.com/java/ 233 | [Go]: https://img.shields.io/badge/Go-00ADD8?style=flat-round&logo=go&logoColor=white 234 | [Go-url]: https://golang.org/ 235 | [Rust]: https://img.shields.io/badge/Rust-000000?style=flat-round&logo=rust&logoColor=white 236 | [Rust-url]: https://www.rust-lang.org/ 237 | [C]: https://img.shields.io/badge/C-00599C?style=flat-round&logo=c&logoColor=white 238 | [C-url]: https://en.wikipedia.org/wiki/C_(programming_language) 239 | [CPP]: https://img.shields.io/badge/C++-00599C?style=flat-round&logo=cplusplus&logoColor=white 240 | [CPP-url]: https://en.wikipedia.org/wiki/C%2B%2B 241 | [CSharp]: https://img.shields.io/badge/C%23-239120?style=flat-round&logo=csharp&logoColor=white 242 | [CSharp-url]: https://docs.microsoft.com/en-us/dotnet/csharp/ 243 | [MATLAB]: https://img.shields.io/badge/MATLAB-0076A8?style=flat-round&logo=mathworks&logoColor=white 244 | [MATLAB-url]: https://www.mathworks.com/products/matlab.html 245 | 246 | 247 | [React.js]: https://img.shields.io/badge/React-20232A?style=flat-round&logo=react&logoColor=61DAFB 248 | [React-url]: https://reactjs.org/ 249 | [Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=flat-round&logo=vuedotjs&logoColor=4FC08D 250 | [Vue-url]: https://vuejs.org/ 251 | [Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=flat-round&logo=angular&logoColor=white 252 | [Angular-url]: https://angular.io/ 253 | [Next.js]: https://img.shields.io/badge/next.js-000000?style=flat-round&logo=nextdotjs&logoColor=white 254 | [Next-url]: https://nextjs.org/ 255 | 256 | 257 | [Flask]: https://img.shields.io/badge/Flask-000000?style=flat-round&logo=flask&logoColor=white 258 | [Flask-url]: https://flask.palletsprojects.com/ 259 | [Django]: https://img.shields.io/badge/Django-092E20?style=flat-round&logo=django&logoColor=white 260 | [Django-url]: https://www.djangoproject.com/ 261 | [FastAPI]: https://img.shields.io/badge/FastAPI-005571?style=flat-round&logo=fastapi&logoColor=white 262 | [FastAPI-url]: https://fastapi.tiangolo.com/ 263 | [Express.js]: https://img.shields.io/badge/Express.js-404D59?style=flat-round&logo=express&logoColor=white 264 | [Express-url]: https://expressjs.com/ 265 | [Spring]: https://img.shields.io/badge/Spring-6DB33F?style=flat-round&logo=spring&logoColor=white 266 | [Spring-url]: https://spring.io/ 267 | [Node.js]: https://img.shields.io/badge/Node.js-43853D?style=flat-round&logo=node.js&logoColor=white 268 | [Node-url]: https://nodejs.org/ 269 | 270 | 271 | [OpenAI]: https://img.shields.io/badge/OpenAI-000000?style=flat-round&logo=openai&logoColor=white 272 | [OpenAI-url]: https://openai.com/ 273 | [Rich]: https://img.shields.io/badge/Rich-000000?style=flat-round&logo=rich&logoColor=white 274 | [Rich-url]: https://rich.readthedocs.io/ 275 | 276 | 277 | [PostgreSQL]: https://img.shields.io/badge/PostgreSQL-316192?style=flat-round&logo=postgresql&logoColor=white 278 | [PostgreSQL-url]: https://www.postgresql.org/ 279 | [MySQL]: https://img.shields.io/badge/MySQL-00000F?style=flat-round&logo=mysql&logoColor=white 280 | [MySQL-url]: https://www.mysql.com/ 281 | [MongoDB]: https://img.shields.io/badge/MongoDB-4EA94B?style=flat-round&logo=mongodb&logoColor=white 282 | [MongoDB-url]: https://www.mongodb.com/ 283 | [Redis]: https://img.shields.io/badge/Redis-DC382D?style=flat-round&logo=redis&logoColor=white 284 | [Redis-url]: https://redis.io/ 285 | [SQLite]: https://img.shields.io/badge/SQLite-07405E?style=flat-round&logo=sqlite&logoColor=white 286 | [SQLite-url]: https://www.sqlite.org/ 287 | -------------------------------------------------------------------------------- /src/readmex/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from pathlib import Path 4 | from typing import Dict, Union, Optional 5 | from rich.console import Console 6 | from rich.panel import Panel 7 | 8 | # Define the global config path 9 | CONFIG_DIR = Path.home() / ".readmex" 10 | CONFIG_FILE = CONFIG_DIR / "config.json" 11 | 12 | # Initialize console for rich printing 13 | console = Console() 14 | 15 | # Global cache for the loaded config 16 | _config_cache: Optional[Dict[str, str]] = None 17 | _config_sources: Optional[Dict[str, str]] = None 18 | 19 | 20 | 21 | def load_config() -> Dict[str, str]: 22 | """ 23 | Load configuration from a global config file and override with environment variables. 24 | Keys from the config file are normalized to lowercase for internal consistency. 25 | """ 26 | global _config_cache, _config_sources 27 | if _config_cache is not None: 28 | return _config_cache 29 | 30 | config = {} 31 | sources = {} 32 | # First, try to load from the global config file 33 | if CONFIG_FILE.exists(): 34 | with open(CONFIG_FILE, 'r') as f: 35 | try: 36 | file_config = json.load(f) 37 | # Normalize keys to lowercase 38 | config = {k.lower(): v for k, v in file_config.items()} 39 | for key in config: 40 | sources[key] = str(CONFIG_FILE) 41 | except json.JSONDecodeError as e: 42 | console.print(Panel(f"[bold red]Error parsing config file:[/] {CONFIG_FILE}\n[bold]Reason:[/] {e}", 43 | title="[bold yellow]Configuration Error[/bold yellow]", expand=False, border_style="red")) 44 | exit() 45 | except AttributeError: 46 | # Handle cases where the file content is not a dictionary 47 | pass 48 | 49 | # Define mapping from ENV var name (uppercase) to internal config key (lowercase) 50 | env_map = { 51 | "LLM_API_KEY": "llm_api_key", 52 | "LLM_BASE_URL": "llm_base_url", 53 | "LLM_MODEL_NAME": "llm_model_name", 54 | "T2I_API_KEY": "t2i_api_key", 55 | "T2I_BASE_URL": "t2i_base_url", 56 | "T2I_MODEL_NAME": "t2i_model_name", 57 | "EMBEDDING_API_KEY": "embedding_api_key", 58 | "EMBEDDING_BASE_URL": "embedding_base_url", 59 | "EMBEDDING_MODEL_NAME": "embedding_model_name", 60 | "LOCAL_EMBEDDING": "local_embedding", 61 | "MAX_WORKERS": "max_workers", 62 | "GITHUB_USERNAME": "github_username", 63 | "TWITTER_HANDLE": "twitter_handle", 64 | "LINKEDIN_USERNAME": "linkedin_username", 65 | "EMAIL": "email", 66 | } 67 | 68 | # Load from environment, overriding file config if an env var is set 69 | for env_var, config_key in env_map.items(): 70 | env_value = os.getenv(env_var) 71 | if env_value is not None: 72 | config[config_key] = env_value 73 | sources[config_key] = f"Environment Variable ({env_var})" 74 | 75 | # Set defaults for personal info if not provided 76 | personal_info_keys = ["github_username", "twitter_handle", "linkedin_username", "email"] 77 | for key in personal_info_keys: 78 | if key not in config: 79 | config[key] = "" 80 | 81 | # Set defaults for embedding config if not provided 82 | embedding_defaults = { 83 | "embedding_model_name": "text-embedding-3-small", 84 | "embedding_base_url": "https://api.openai.com/v1", 85 | "embedding_api_key": "", 86 | "local_embedding": "true" 87 | } 88 | for key, default_value in embedding_defaults.items(): 89 | if key not in config: 90 | config[key] = default_value 91 | 92 | # Set defaults for max_workers if not provided 93 | if "max_workers" not in config: 94 | config["max_workers"] = "10" 95 | 96 | _config_cache = config 97 | _config_sources = sources 98 | return _config_cache 99 | 100 | def validate_config(): 101 | """Validate that required configurations are present and provide detailed guidance if not.""" 102 | config = load_config() 103 | required_keys = ["llm_api_key", "t2i_api_key"] 104 | missing_keys = [key for key in required_keys if not config.get(key)] 105 | 106 | if missing_keys: 107 | console.print(Panel("[bold yellow]Missing required API keys. Let's set them up.[/bold yellow]", title="Configuration Required", expand=False)) 108 | 109 | # Interactive input for missing keys 110 | for key in missing_keys: 111 | config[key] = console.input(f"Please enter your [bold cyan]{key}[/bold cyan]: ").strip() 112 | 113 | # After LLM API key is entered, prompt for LLM base URL 114 | if key == "llm_api_key": 115 | llm_base_url = console.input("Please enter your llm_base_url(default is https://api.openai.com/v1): ").strip() 116 | if llm_base_url: 117 | config["llm_base_url"] = llm_base_url 118 | 119 | # Save to config file 120 | CONFIG_DIR.mkdir(exist_ok=True) 121 | 122 | # Load existing config to update it, not overwrite 123 | try: 124 | with open(CONFIG_FILE, 'r') as f_read: 125 | existing_config = json.load(f_read) 126 | except (FileNotFoundError, json.JSONDecodeError): 127 | # Create a complete config template with all possible keys 128 | existing_config = { 129 | "LLM_API_KEY": "", 130 | "LLM_BASE_URL": "https://api.openai.com/v1", 131 | "LLM_MODEL_NAME": "gpt-3.5-turbo", 132 | "T2I_API_KEY": "", 133 | "T2I_BASE_URL": "https://api.openai.com/v1", 134 | "T2I_MODEL_NAME": "dall-e-3", 135 | "EMBEDDING_API_KEY": "", 136 | "EMBEDDING_BASE_URL": "https://api.openai.com/v1", 137 | "EMBEDDING_MODEL_NAME": "text-embedding-3-small", 138 | "LOCAL_EMBEDDING": "true", 139 | "MAX_WORKERS": "10", 140 | "GITHUB_USERNAME": "", 141 | "TWITTER_HANDLE": "", 142 | "LINKEDIN_USERNAME": "", 143 | "EMAIL": "" 144 | } 145 | 146 | # Update with user input 147 | existing_config.update({k.upper(): v for k, v in config.items() if v}) # Save keys in uppercase 148 | 149 | with open(CONFIG_FILE, 'w') as f: 150 | json.dump(existing_config, f, indent=2) 151 | 152 | console.print(f"[green]✔ Configuration saved to [bold cyan]{CONFIG_FILE}[/bold cyan][/green]") 153 | 154 | # Reload config to ensure it's up-to-date 155 | global _config_cache 156 | _config_cache = None 157 | load_config() 158 | 159 | 160 | def get_config_sources() -> Dict[str, str]: 161 | """Returns the sources of the configuration values.""" 162 | global _config_sources 163 | if _config_sources is None: 164 | load_config() 165 | return _config_sources 166 | 167 | 168 | def get_llm_config() -> Dict[str, Union[str, int, float]]: 169 | config = load_config() 170 | return { 171 | "model_name": config.get("llm_model_name", "gpt-3.5-turbo"), 172 | "base_url": config.get("llm_base_url", "https://api.openai.com/v1"), 173 | "api_key": config.get("llm_api_key"), 174 | "max_tokens": int(config.get("llm_max_tokens", 1024)), 175 | "temperature": float(config.get("llm_temperature", 0.7)), 176 | } 177 | 178 | 179 | def get_t2i_config() -> Dict[str, Union[str, int, float]]: 180 | config = load_config() 181 | return { 182 | "model_name": config.get("t2i_model_name", "dall-e-3"), 183 | "base_url": config.get("t2i_base_url", "https://api.openai.com/v1"), 184 | "api_key": config.get("t2i_api_key"), 185 | "size": config.get("t2i_image_size", "1024x1024"), 186 | "quality": config.get("t2i_image_quality", "standard"), 187 | } 188 | 189 | 190 | def get_embedding_config() -> Dict[str, Union[str, bool]]: 191 | """获取embedding模型配置""" 192 | config = load_config() 193 | return { 194 | "model_name": config.get("embedding_model_name", "text-embedding-3-small"), 195 | "base_url": config.get("embedding_base_url", "https://api.openai.com/v1"), 196 | "api_key": config.get("embedding_api_key"), 197 | "local_embedding": config.get("local_embedding", "true").lower() == "true", 198 | } 199 | 200 | 201 | def get_max_workers() -> int: 202 | """获取最大并发工作线程数""" 203 | config = load_config() 204 | try: 205 | return int(config.get("max_workers", "10")) 206 | except (ValueError, TypeError): 207 | return 10 208 | 209 | 210 | # Keep original default configurations for use by other modules 211 | DEFAULT_IGNORE_PATTERNS = [ 212 | ".git", 213 | ".github", 214 | ".cursor", 215 | ".cursorrules", 216 | ".vscode", 217 | "__pycache__", 218 | "*.pyc", 219 | "*.md", 220 | "website/*", 221 | ".DS_Store", 222 | "build", 223 | "dist", 224 | "*.egg-info", 225 | ".venv", 226 | "venv", 227 | "__init__.py", # 根目录下的 __init__.py 228 | "*/__init__.py", # 一级子目录下的 __init__.py 229 | "*/*/__init__.py", # 二级子目录下的 __init__.py 230 | ".idea", 231 | "*output*" 232 | ] 233 | 234 | # Patterns for script files to be described by the LLM 235 | SCRIPT_PATTERNS = ["*.py", "*.sh", "*.ipynb"] 236 | DOCUMENT_PATTERNS = ["*.md", "*.txt"] 237 | 238 | 239 | def get_readme_template_path(): 240 | """Gets the path to the BLANK_README.md template.""" 241 | from importlib import resources 242 | import os 243 | 244 | try: 245 | # 尝试使用 importlib.resources (Python 3.9+) 246 | template_files = resources.files('readmex.templates') 247 | if template_files is not None: 248 | template_path = template_files.joinpath('BLANK_README.md') 249 | if template_path.is_file(): 250 | return str(template_path) 251 | except (AttributeError, FileNotFoundError, TypeError): 252 | pass 253 | 254 | # 备用方案:使用相对路径 255 | try: 256 | current_dir = os.path.dirname(os.path.abspath(__file__)) 257 | template_path = os.path.join(current_dir, 'templates', 'BLANK_README.md') 258 | if os.path.exists(template_path): 259 | return template_path 260 | except Exception: 261 | pass 262 | 263 | # 最后的备用方案:使用 pkg_resources (如果可用) 264 | try: 265 | import pkg_resources 266 | return pkg_resources.resource_filename('readmex.templates', 'BLANK_README.md') 267 | except (ImportError, FileNotFoundError): 268 | pass 269 | 270 | raise FileNotFoundError("BLANK_README.md not found in package templates.") 271 | 272 | 273 | if __name__ == "__main__": 274 | # Test configuration loading 275 | validate_config() 276 | print("=== LLM Configuration ===") 277 | llm_config = get_llm_config() 278 | for key, value in llm_config.items(): 279 | print(f"{key}: {value}") 280 | 281 | print("\n=== Text-to-Image Configuration ===") 282 | t2i_config = get_t2i_config() 283 | for key, value in t2i_config.items(): 284 | print(f"{key}: {value}") 285 | 286 | print("\n=== Configuration Validation ===") 287 | try: 288 | validate_config() 289 | print("Configuration validation passed") 290 | except SystemExit: 291 | # The validate_config function calls exit(), so we catch it to allow the script to continue 292 | # In a real run, the program would terminate here. 293 | pass 294 | except ValueError as e: 295 | print(f"Configuration validation failed: {e}") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | English | [简体中文](README_CN.md) 8 | 9 |
10 | 11 | 12 |
13 | Poster 14 |
15 | 16 | --- 17 | 18 | 19 |
20 |
21 | 24 | 25 |

readmex

26 | 27 |

28 | 🚀 AI-Powered README Generator: Automatically creates beautiful READMEs and interactive wikis for any repository! Can run all in local with your own models. 29 |
30 | Explore the docs » 31 |
32 |

33 | 34 | 35 | [![Contributors][contributors-shield]][contributors-url] 36 | [![Forks][forks-shield]][forks-url] 37 | [![Stargazers][stars-shield]][stars-url] 38 | [![Issues][issues-shield]][issues-url] 39 | 41 | [![License][license-shield]][license-url] 42 | 43 |

44 | View Demo 45 | · 46 | Report Bug 47 | · 48 | Request Feature 49 |

50 |
51 | 52 | 53 | 54 | 55 |
56 | Table of Contents 57 |
    58 |
  1. 59 | About The Project 60 | 63 |
  2. 64 |
  3. 65 | Getting Started 66 | 70 |
  4. 71 |
  5. Usage
  6. 72 |
  7. Roadmap
  8. 73 |
  9. Contributing
  10. 74 |
  11. License
  12. 75 |
  13. Contact
  14. 76 |
  15. Acknowledgments
  16. 77 |
78 |
79 | 80 | 81 | 82 | 83 | ## 📖 About The Project 84 | 85 | [![Flow Chart](images/flow.png)](https://example.com) 86 | 87 | AI-Powered README Generator is an AI-powered tool that automatically generates comprehensive Markdown README files for your projects. It crafts well-structured documentation that includes project details, technology stack, setup instructions, usage examples, badges, logos, and more. 88 | 89 | ### Key Features 90 | 91 | - 🤖 **AI-Powered READMEs**: Generate comprehensive Markdown READMEs instantly. 92 | - 🔗 **Auto Badges**: Creates and embeds relevant status badges (contributors, forks, stars, etc.). 93 | - 🖼️ **Smart Logo Design**: Crafts a unique project logo automatically. 94 | - 🧠 **Tech Stack Identification**: Automatically detects and includes the project's technology stack. 95 | - 🌐 **Context-Aware Intelligence**: Tailors content to your project's specific context and needs. 96 | 97 |

(back to top)

98 | 99 | 100 | 101 | ### Built With 102 | 103 | - [![Python][Python]][Python-url] 104 | - [![OpenAI][OpenAI]][OpenAI-url] 105 | - [![Rich][Rich]][Rich-url] 106 | 107 | ### Supported Programming Languages 108 | 109 |
110 | Click to expand supported languages 111 | 112 | #### Web Development 113 | - **JavaScript** 114 | - **TypeScript** 115 | - **HTML** 116 | - **CSS** 117 | - **SCSS** 118 | - **Sass** 119 | - **Less** 120 | - **Stylus** 121 | - **Pug** 122 | - **Handlebars** 123 | - **Mustache** 124 | - **Twig** 125 | - **Smarty** 126 | - **Jinja** 127 | - **Vue** 128 | 129 | #### Programming Languages 130 | - **Python** 131 | - **Java** 132 | - **C** 133 | - **C++** 134 | - **C#** 135 | - **Go** 136 | - **Rust** 137 | - **PHP** 138 | - **Ruby** 139 | - **Swift** 140 | - **Kotlin** 141 | - **Scala** 142 | - **R** 143 | - **MATLAB** 144 | - **Perl** 145 | - **Lua** 146 | - **Dart** 147 | - **F#** 148 | - **Visual Basic** 149 | - **Assembly** 150 | - **Objective-C** 151 | - **Haskell** 152 | - **Erlang** 153 | - **Elixir** 154 | - **Clojure** 155 | - **CoffeeScript** 156 | - **PowerShell** 157 | - **Shell** 158 | - **Batch** 159 | - **Solidity** 160 | 161 | #### Build & Configuration 162 | - **Dockerfile** 163 | - **Makefile** 164 | - **CMake** 165 | - **Gradle** 166 | - **Maven** 167 | - **Nix** 168 | - **Terraform** 169 | 170 | #### Data & Documentation 171 | - **Jupyter** 172 | - **Protobuf** 173 | - **GraphQL** 174 | - **WebAssembly** 175 | 176 | #### Editor & IDE 177 | - **Vim** 178 | - **Emacs** 179 | 180 |
181 | 182 |

(back to top)

183 | 184 | 185 | 186 | 187 | ## 🚀 Getting Started 188 | 189 | This is an example of how you may give instructions on setting up your project locally. To get a local copy up and running follow these simple steps. 190 | 191 | ### Prerequisites 192 | 193 | - Python 3.7+ 194 | 195 | ### Installation 196 | 197 | 1. Install the package using pip: 198 | ```bash 199 | pip install readmex 200 | ``` 201 | ### Configuration 202 | 203 | `readmex` requires API keys for both the Language Model (for generating text) and the Text-to-Image model (for generating logos). You can configure these in one of two ways. Environment variables take precedence. 204 | 205 | #### 1. Environment Variables (Recommended for CI/CD) 206 | 207 | Set the following environment variables in your shell: 208 | 209 | ```bash 210 | export LLM_API_KEY="your_llm_api_key" # Required 211 | export T2I_API_KEY="your_t2i_api_key" # Required 212 | 213 | # Optional: Specify custom API endpoints and models 214 | export LLM_BASE_URL="https://api.example.com/v1" 215 | export T2I_BASE_URL="https://api.example.com/v1" 216 | export LLM_MODEL_NAME="your-llm-model" 217 | export T2I_MODEL_NAME="your-t2i-model" 218 | 219 | # Optional: Embedding model configuration for RAG (Retrieval-Augmented Generation) 220 | export EMBEDDING_API_KEY="your_embedding_api_key" # Optional, for web embedding models 221 | export EMBEDDING_BASE_URL="https://api.example.com/v1" # Optional, for web embedding models 222 | export EMBEDDING_MODEL_NAME="text-embedding-3-small" # Optional, embedding model name 223 | export LOCAL_EMBEDDING="true" # Optional, use local embedding model (default: true) 224 | 225 | # Optional: Performance configuration 226 | export MAX_WORKERS="10" # Optional, max concurrent threads (default: 10) 227 | ``` 228 | 229 | #### 2. Global Config File (Recommended for Local Use) 230 | 231 | For convenience, you can create a global configuration file. The tool will automatically look for it. 232 | 233 | 1. Create the directory: `mkdir -p ~/.readmex` 234 | 2. Create the config file: `~/.readmex/config.json` 235 | 3. Add your credentials and any optional settings. You can also include personal information, which will be used as defaults during interactive prompts: 236 | 237 | ```json 238 | { 239 | "LLM_API_KEY": "your_llm_api_key", 240 | "T2I_API_KEY": "your_t2i_api_key", 241 | "LLM_BASE_URL": "https://api.example.com/v1", 242 | "T2I_BASE_URL": "https://api.example.com/v1", 243 | "LLM_MODEL_NAME": "gpt-4", 244 | "T2I_MODEL_NAME": "dall-e-3", 245 | "EMBEDDING_API_KEY": "your_embedding_api_key", 246 | "EMBEDDING_BASE_URL": "https://api.example.com/v1", 247 | "EMBEDDING_MODEL_NAME": "text-embedding-3-small", 248 | "LOCAL_EMBEDDING": "true", 249 | "MAX_WORKERS": "10", 250 | "github_username": "your_github_username", 251 | "twitter_handle": "your_twitter_handle", 252 | "linkedin_username": "your_linkedin_username", 253 | "email": "your_email@example.com" 254 | } 255 | ``` 256 | 257 |

(back to top)

258 | 259 | 260 | 261 | 262 | ## 💻 Usage 263 | 264 | Once installed, you can use the `readmex` package in the command line. To generate your README, run the following: 265 | 266 | ### Method 1: Using the installed command (Recommended) 267 | ```bash 268 | readmex 269 | ``` 270 | 271 | ### Method 2: Running as a Python module 272 | ```bash 273 | # Run the package directly 274 | python -m readmex 275 | 276 | # Or run the CLI module specifically 277 | python -m readmex.utils.cli 278 | ``` 279 | 280 | ### Method 3: Development mode (for contributors) 281 | ```bash 282 | # From the project root directory 283 | python src/readmex/utils/cli.py 284 | ``` 285 | 286 | ### Command Line Options 287 | 288 | All methods support the same command line arguments: 289 | 290 | ```bash 291 | # Interactive mode (default) 292 | readmex 293 | 294 | # Generate for current directory 295 | readmex . 296 | 297 | # Generate for specific directory 298 | readmex /path/to/your/project 299 | 300 | # Generate MkDocs website 301 | readmex --website 302 | 303 | # Generate website and serve locally 304 | readmex --website --serve 305 | 306 | # Deploy to GitHub Pages 307 | readmex --deploy 308 | 309 | # Enable debug mode (skip LLM calls for testing) 310 | readmex --debug 311 | 312 | # Enable silent mode (auto-generate without prompts) 313 | readmex --silent 314 | 315 | # Enable verbose mode (show detailed information) 316 | readmex --verbose 317 | ``` 318 | 319 | This will: 320 | 1. generate a `project_structure.txt` file, which contains the project structure. 321 | 2. generate a `script_description.json` file, which contains the description of the scripts in the project. 322 | 3. generate a `requirements.txt` file, which contains the requirements of the project. 323 | 4. generate a `logo.png` file, which contains the logo of the project. 324 | 5. generate a `README.md` file, which contains the README of the project. 325 | 326 |

(back to top)

327 | 328 | 329 | 330 | 331 | ## 🗺️ Roadmap 332 | 333 | - [ ] Prompt Engineering for Logo Generation 334 | - [ ] Multi-language Support 335 | - [ ] Enhanced AI Descriptions for Project Features 336 | 337 | See the [open issues](https://github.com/aibox22/readmex/issues) for a full list of proposed features (and known issues). 338 | 339 |

(back to top)

340 | 341 | 342 | 343 | 344 | ## 🤝 Contributing 345 | 346 | Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 347 | 348 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 349 | Don't forget to give the project a star! Thanks again! 350 | 351 | 1. Fork the Project 352 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 353 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 354 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 355 | 5. Open a Pull Request 356 | 357 |

(back to top)

358 | 359 | ### Top contributors: 360 | 361 | 362 | contrib.rocks image 363 | 364 | 365 | 366 | 367 | 368 | ## 🎗 License 369 | 370 | Copyright © 2024-2025 [readmex][readmex].
371 | Released under the [MIT][license-url] license. 372 | 373 |

(back to top)

374 | 375 | 376 | 377 | 378 | ## 📧 Contact 379 | 380 | Email: lintaothu@foxmail.com 381 | 382 | Project Link: [https://github.com/aibox22/readmex](https://github.com/aibox22/readmex) 383 | 384 | QQ Group: 2161023585 (Welcome to join our QQ Group to discuss and get help!) 385 | 386 |
387 | QQ Group QR Code 388 |

Scan QR code to join our QQ Group

389 |
390 | 391 |

(back to top)

392 | 393 | 394 | 395 | [readmex]: https://github.com/aibox22/readmex 396 | 397 | 398 | [contributors-shield]: https://img.shields.io/github/contributors/aibox22/readmex.svg?style=flat-round 399 | [contributors-url]: https://github.com/aibox22/readmex/graphs/contributors 400 | [forks-shield]: https://img.shields.io/github/forks/aibox22/readmex.svg?style=flat-round 401 | [forks-url]: https://github.com/aibox22/readmex/network/members 402 | [stars-shield]: https://img.shields.io/github/stars/aibox22/readmex.svg?style=flat-round 403 | [stars-url]: https://github.com/aibox22/readmex/stargazers 404 | [issues-shield]: https://img.shields.io/github/issues/aibox22/readmex.svg?style=flat-round 405 | [issues-url]: https://github.com/aibox22/readmex/issues 406 | [release-shield]: https://img.shields.io/github/v/release/aibox22/readmex?style=flat-round 407 | [release-url]: https://github.com/aibox22/readmex/releases 408 | [release-date-shield]: https://img.shields.io/github/release-date/aibox22/readmex?color=9cf&style=flat-round 409 | [license-shield]: https://img.shields.io/github/license/aibox22/readmex.svg?style=flat-round 410 | [license-url]: https://github.com/aibox22/readmex/blob/master/LICENSE.txt 411 | [Python]: https://img.shields.io/badge/Python-3776AB?style=flat-round&logo=python&logoColor=white 412 | [Python-url]: https://www.python.org/ 413 | [OpenAI]: https://img.shields.io/badge/OpenAI-000000?style=flat-round&logo=openai&logoColor=white 414 | [OpenAI-url]: https://openai.com/ 415 | [Flask]: https://img.shields.io/badge/Flask-000000?style=flat-round&logo=flask&logoColor=white 416 | [Flask-url]: https://flask.palletsprojects.com/ 417 | [Rich]: https://img.shields.io/badge/Rich-000000?style=flat-round&logo=rich&logoColor=white 418 | [Rich-url]: https://rich.readthedocs.io/ 419 | 420 | 421 | ## ⭐ Star History 422 | 423 |
424 | 425 | Star History Chart 426 | 427 |
428 | -------------------------------------------------------------------------------- /src/readmex/utils/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | from pathlib import Path 5 | from rich.console import Console 6 | from rich.table import Table 7 | 8 | # Add the src directory to the Python path when running directly 9 | if __name__ == '__main__': 10 | # Get the project root directory (3 levels up from this file) 11 | project_root = Path(__file__).parent.parent.parent.parent 12 | src_path = project_root / "src" 13 | if src_path not in sys.path: 14 | sys.path.insert(0, str(src_path)) 15 | 16 | from readmex.core import readmex 17 | from readmex.website_core import WebsiteGenerator 18 | from readmex.config import validate_config, get_config_sources 19 | 20 | def main(): 21 | """ 22 | readmex command line entry point 23 | Support both command line arguments and interactive interface 24 | """ 25 | parser = argparse.ArgumentParser( 26 | description="readmex - AI-driven README documentation generator", 27 | epilog="Examples:\n readmex # Interactive mode\n readmex . # Generate for current directory\n readmex ./my-project # Generate for specific directory\n readmex --website # Generate MkDocs website\n readmex --website --serve # Generate and serve website", 28 | formatter_class=argparse.RawDescriptionHelpFormatter 29 | ) 30 | parser.add_argument( 31 | "project_path", 32 | nargs="?", 33 | help="Path of project for generating README (default: interactive input)" 34 | ) 35 | parser.add_argument( 36 | "--website", 37 | action="store_true", 38 | help="Generate MkDocs website instead of README" 39 | ) 40 | parser.add_argument( 41 | "--serve", 42 | action="store_true", 43 | help="Start local server after generating website (requires --website)" 44 | ) 45 | parser.add_argument( 46 | "--deploy", 47 | action="store_true", 48 | help="Deploy website to GitHub Pages (requires --website)" 49 | ) 50 | parser.add_argument( 51 | "--version", 52 | action="version", 53 | version="readmex 0.1.8" 54 | ) 55 | parser.add_argument( 56 | "--verbose", 57 | action="store_true", 58 | help="Enable verbose mode to print prompts and detailed information" 59 | ) 60 | parser.add_argument( 61 | "--debug", 62 | action="store_true", 63 | help="Enable debug mode to skip LLM calls for faster testing" 64 | ) 65 | parser.add_argument( 66 | "--silent", 67 | action="store_true", 68 | help="Enable silent mode to skip interactive prompts (auto-generate all content)" 69 | ) 70 | 71 | args = parser.parse_args() 72 | 73 | try: 74 | validate_config() 75 | console = Console() 76 | 77 | # Determine project path 78 | if args.project_path: 79 | project_path = os.path.abspath(args.project_path) 80 | if not os.path.isdir(project_path): 81 | console.print(f"[bold red]Error: Project path '{project_path}' is not a valid directory.[/bold red]") 82 | return 83 | elif args.serve: 84 | # 对于 --serve 参数,直接使用当前目录 85 | project_path = os.getcwd() 86 | else: 87 | console.print("[bold cyan]readmex - AI README Generator[/bold cyan]") 88 | console.print("Please provide the path of project for generating README (press Enter to use the current directory).\n") 89 | project_input = console.input("[cyan]Project Path[/cyan]: ").strip() 90 | project_path = os.path.abspath(project_input) if project_input else os.getcwd() 91 | 92 | if not os.path.isdir(project_path): 93 | console.print(f"[bold red]Error: Project path '{project_path}' is not a valid directory.[/bold red]") 94 | return 95 | 96 | if args.website: 97 | # 网站生成模式 98 | _handle_website_generation(args, project_path, console) 99 | elif args.serve: 100 | # 仅启动服务模式 101 | _handle_serve_only(project_path, console) 102 | else: 103 | # README生成模式 104 | generator = readmex( 105 | project_dir=project_path, 106 | debug=getattr(args, 'debug', False), 107 | silent=getattr(args, 'silent', False) 108 | ) 109 | generator.generate() 110 | except KeyboardInterrupt: 111 | console = Console() 112 | console.print("\n[yellow]Operation cancelled[/yellow]") 113 | except FileNotFoundError as e: 114 | console = Console() 115 | console.print(f"[red]Error: {e}[/red]") 116 | except Exception as e: 117 | console = Console() 118 | console.print(f"[bold red]An error occurred: {e}[/bold red]") 119 | 120 | # Show configuration information to help with debugging 121 | from readmex.config import load_config 122 | try: 123 | config = load_config() 124 | sources = get_config_sources() 125 | if config and sources: 126 | # Show configuration source info once 127 | console.print("\n[yellow]Configuration loaded from:[/yellow]") 128 | source_files = set(sources.values()) 129 | for source_file in source_files: 130 | if "Environment Variable" not in source_file: 131 | console.print(f"[yellow] • {source_file}[/yellow]") 132 | 133 | # Show configuration table with actual values 134 | table = Table(title="[bold cyan]Current Configuration[/bold cyan]") 135 | table.add_column("Variable", style="cyan") 136 | table.add_column("Value", style="green") 137 | 138 | # Only show non-sensitive configuration values 139 | display_keys = ["llm_model_name", "t2i_model_name", "llm_base_url", "t2i_base_url", 140 | "embedding_model_name", "embedding_base_url", "local_embedding", 141 | "github_username", "twitter_handle", "linkedin_username", "email"] 142 | 143 | for key in display_keys: 144 | if key in config and config[key]: 145 | value = config[key] 146 | # Mask API keys for security 147 | if "api_key" in key.lower(): 148 | value = "***" + value[-4:] if len(value) > 4 else "***" 149 | table.add_row(key, value) 150 | 151 | console.print(table) 152 | except Exception: 153 | pass # Don't show config info if there's an error loading it 154 | 155 | 156 | def _handle_serve_only(project_path: str, console: Console) -> None: 157 | """处理仅启动服务功能""" 158 | try: 159 | # 创建网站生成器以获取输出目录 160 | website_generator = WebsiteGenerator(project_path) 161 | 162 | # 检查网站是否存在 163 | website_exists = os.path.exists(website_generator.output_dir) and \ 164 | os.path.exists(os.path.join(website_generator.output_dir, "mkdocs.yml")) and \ 165 | os.path.exists(os.path.join(website_generator.output_dir, "docs")) 166 | 167 | if not website_exists: 168 | console.print("[red]未找到已生成的网站。请先运行 'readmex --website' 生成网站。[/red]") 169 | return 170 | 171 | console.print("[green]启动网站服务...[/green]") 172 | _serve_website(website_generator.output_dir, console) 173 | 174 | except Exception as e: 175 | console.print(f"[red]启动服务失败: {e}[/red]") 176 | raise 177 | 178 | 179 | def _handle_website_generation(args, project_path: str, console: Console) -> None: 180 | """处理网站生成相关功能""" 181 | try: 182 | # 验证参数组合 183 | if args.deploy and not args.website: 184 | console.print("[red]Error: --deploy requires --website[/red]") 185 | return 186 | 187 | # 创建网站生成器 188 | website_generator = WebsiteGenerator(project_path, verbose=getattr(args, 'verbose', False), debug=getattr(args, 'debug', False)) 189 | 190 | # 检查是否只需要启动服务且网站已存在 191 | website_exists = os.path.exists(website_generator.output_dir) and \ 192 | os.path.exists(os.path.join(website_generator.output_dir, "mkdocs.yml")) and \ 193 | os.path.exists(os.path.join(website_generator.output_dir, "docs")) 194 | 195 | if args.serve and website_exists and not args.deploy: 196 | # 如果只是要启动服务且网站已存在,直接启动服务 197 | console.print("[green]检测到已存在的网站,直接启动服务...[/green]") 198 | _serve_website(website_generator.output_dir, console) 199 | return 200 | 201 | # 生成网站 202 | website_generator.generate_website() 203 | 204 | # 处理后续操作 205 | if args.serve: 206 | _serve_website(website_generator.output_dir, console) 207 | elif args.deploy: 208 | _deploy_website(website_generator.output_dir, console) 209 | 210 | except Exception as e: 211 | console.print(f"[red]网站生成失败: {e}[/red]") 212 | raise 213 | 214 | 215 | def _serve_website(website_dir: str, console: Console) -> None: 216 | """启动本地服务器""" 217 | try: 218 | import subprocess 219 | import webbrowser 220 | import time 221 | import socket 222 | 223 | console.print("[cyan]启动本地服务器...[/cyan]") 224 | 225 | # 检查是否安装了mkdocs 226 | try: 227 | subprocess.run(["mkdocs", "--version"], 228 | capture_output=True, check=True) 229 | except (subprocess.CalledProcessError, FileNotFoundError): 230 | console.print("[yellow]未找到mkdocs,正在安装...[/yellow]") 231 | subprocess.run(["pip3.9", "install", "mkdocs", "mkdocs-material", "mkdocs-drawio"], 232 | check=True) 233 | 234 | # 检查端口是否被占用 235 | def is_port_in_use(port): 236 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 237 | return s.connect_ex(('localhost', port)) == 0 238 | 239 | port = 8000 240 | if is_port_in_use(port): 241 | console.print(f"[yellow]端口 {port} 已被占用,尝试使用其他端口...[/yellow]") 242 | for p in range(8001, 8010): 243 | if not is_port_in_use(p): 244 | port = p 245 | break 246 | else: 247 | console.print("[red]无法找到可用端口[/red]") 248 | return 249 | 250 | # 启动服务器 251 | console.print("[green]服务器启动中,请稍候...[/green]") 252 | process = subprocess.Popen( 253 | ["mkdocs", "serve", "-a", f"127.0.0.1:{port}"], 254 | cwd=website_dir, 255 | stdout=subprocess.PIPE, 256 | stderr=subprocess.STDOUT, 257 | text=True 258 | ) 259 | 260 | # 等待服务器启动并检查是否成功 261 | max_wait_time = 10 262 | start_time = time.time() 263 | server_started = False 264 | 265 | while time.time() - start_time < max_wait_time: 266 | if process.poll() is not None: 267 | # 进程已退出,说明启动失败 268 | output = process.stdout.read() if process.stdout else "" 269 | console.print(f"[red]服务器启动失败: {output}[/red]") 270 | return 271 | 272 | if is_port_in_use(port): 273 | server_started = True 274 | break 275 | 276 | time.sleep(0.5) 277 | 278 | if not server_started: 279 | console.print("[red]服务器启动超时[/red]") 280 | process.terminate() 281 | return 282 | 283 | # 打开浏览器 284 | url = f"http://127.0.0.1:{port}" 285 | console.print(f"[green]✅ 服务器已启动: {url}[/green]") 286 | 287 | open_browser = console.input( 288 | "[cyan]是否在浏览器中打开网站? (Y/n): [/cyan]" 289 | ).strip().lower() 290 | 291 | if open_browser in ['', 'y', 'yes', '是']: 292 | webbrowser.open(url) 293 | 294 | console.print("[yellow]按 Ctrl+C 停止服务器[/yellow]") 295 | 296 | try: 297 | # 持续监控服务器状态 298 | while process.poll() is None: 299 | time.sleep(1) 300 | except KeyboardInterrupt: 301 | console.print("\n[yellow]正在停止服务器...[/yellow]") 302 | process.terminate() 303 | try: 304 | process.wait(timeout=5) 305 | except subprocess.TimeoutExpired: 306 | process.kill() 307 | console.print("[green]服务器已停止[/green]") 308 | 309 | except Exception as e: 310 | console.print(f"[red]启动服务器失败: {e}[/red]") 311 | 312 | 313 | def _deploy_website(website_dir: str, console: Console) -> None: 314 | """部署网站到GitHub Pages""" 315 | try: 316 | import subprocess 317 | 318 | console.print("[cyan]准备部署到GitHub Pages...[/cyan]") 319 | 320 | # 检查是否在git仓库中 321 | try: 322 | subprocess.run(["git", "status"], 323 | cwd=website_dir, 324 | capture_output=True, 325 | check=True) 326 | except subprocess.CalledProcessError: 327 | console.print("[red]Error: 当前目录不是git仓库[/red]") 328 | return 329 | 330 | # 检查是否安装了mkdocs 331 | try: 332 | subprocess.run(["mkdocs", "--version"], 333 | capture_output=True, check=True) 334 | except (subprocess.CalledProcessError, FileNotFoundError): 335 | console.print("[yellow]未找到mkdocs,正在安装...[/yellow]") 336 | subprocess.run(["pip", "install", "mkdocs", "mkdocs-material", "mkdocs-drawio"], 337 | check=True) 338 | 339 | # 部署到gh-pages分支 340 | console.print("[cyan]正在部署...[/cyan]") 341 | result = subprocess.run( 342 | ["mkdocs", "gh-deploy", "--clean"], 343 | cwd=website_dir, 344 | capture_output=True, 345 | text=True 346 | ) 347 | 348 | if result.returncode == 0: 349 | console.print("[green]✅ 网站已成功部署到GitHub Pages[/green]") 350 | console.print("[cyan]通常需要几分钟时间生效[/cyan]") 351 | else: 352 | console.print(f"[red]部署失败: {result.stderr}[/red]") 353 | 354 | except Exception as e: 355 | console.print(f"[red]部署失败: {e}[/red]") 356 | 357 | 358 | if __name__ == '__main__': 359 | main() -------------------------------------------------------------------------------- /src/readmex/config/dependency_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": { 3 | "python": { 4 | "dependency_files": ["requirements.txt", "pyproject.toml", "setup.py", "Pipfile"], 5 | "file_extensions": ["*.py"], 6 | "import_patterns": [ 7 | { 8 | "pattern": "^import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", 9 | "format": "import {module}", 10 | "group": 1 11 | }, 12 | { 13 | "pattern": "^from\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\s+import\\s+(.+)", 14 | "format": "from {module} import {items}", 15 | "group": 1, 16 | "items_group": 2 17 | } 18 | ], 19 | "prompt_template": "Based on the following import statements from a Python project, generate a requirements.txt file with appropriate package versions.\n\nImport statements found:\n{imports}\n\nExisting requirements.txt (if any):\n{existing}\n\nPlease generate a complete requirements.txt file that includes:\n1. Only external packages (not built-in Python modules)\n2. Reasonable version specifications (use >= for flexibility)\n3. Common packages with their typical versions\n4. Merge with existing requirements if provided\n\nReturn only the requirements.txt content, one package per line in format: package>=version" 20 | }, 21 | "javascript": { 22 | "dependency_files": ["package.json", "yarn.lock", "package-lock.json"], 23 | "file_extensions": ["*.js", "*.jsx", "*.ts", "*.tsx"], 24 | "import_patterns": [ 25 | { 26 | "pattern": "^import\\s+.*?from\\s+['\"]([^'\"]+)['\"]", 27 | "format": "import from '{module}'", 28 | "group": 1 29 | }, 30 | { 31 | "pattern": "^const\\s+.*?=\\s+require\\(['\"]([^'\"]+)['\"]\\)", 32 | "format": "require('{module}')", 33 | "group": 1 34 | }, 35 | { 36 | "pattern": "^import\\s+['\"]([^'\"]+)['\"]", 37 | "format": "import '{module}'", 38 | "group": 1 39 | } 40 | ], 41 | "prompt_template": "Based on the following import statements from a JavaScript/TypeScript project, generate a package.json dependencies section.\n\nImport statements found:\n{imports}\n\nExisting package.json (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external packages (not relative imports)\n2. Reasonable version specifications (use ^ for flexibility)\n3. Common packages with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the dependencies object in JSON format." 42 | }, 43 | "java": { 44 | "dependency_files": ["pom.xml", "build.gradle", "gradle.properties"], 45 | "file_extensions": ["*.java"], 46 | "import_patterns": [ 47 | { 48 | "pattern": "^import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*);", 49 | "format": "import {module}", 50 | "group": 1 51 | }, 52 | { 53 | "pattern": "^import\\s+static\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*);", 54 | "format": "import static {module}", 55 | "group": 1 56 | } 57 | ], 58 | "prompt_template": "Based on the following import statements from a Java project, generate Maven dependencies or Gradle dependencies.\n\nImport statements found:\n{imports}\n\nExisting build file (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external packages (not java.* or javax.* built-in packages)\n2. Common libraries with their typical versions\n3. Merge with existing dependencies if provided\n\nReturn the dependencies in Maven XML format or Gradle format as appropriate." 59 | }, 60 | "go": { 61 | "dependency_files": ["go.mod", "go.sum"], 62 | "file_extensions": ["*.go"], 63 | "import_patterns": [ 64 | { 65 | "pattern": "^import\\s+\"([^\"]+)\"", 66 | "format": "import \"{module}\"", 67 | "group": 1 68 | }, 69 | { 70 | "pattern": "^import\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s+\"([^\"]+)\"", 71 | "format": "import \"{module}\"", 72 | "group": 1 73 | } 74 | ], 75 | "prompt_template": "Based on the following import statements from a Go project, generate a go.mod file.\n\nImport statements found:\n{imports}\n\nExisting go.mod (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external packages (not standard library)\n2. Reasonable version specifications\n3. Common packages with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the go.mod content." 76 | }, 77 | "rust": { 78 | "dependency_files": ["Cargo.toml", "Cargo.lock"], 79 | "file_extensions": ["*.rs"], 80 | "import_patterns": [ 81 | { 82 | "pattern": "^use\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z_][a-zA-Z0-9_]*)*)", 83 | "format": "use {module}", 84 | "group": 1 85 | }, 86 | { 87 | "pattern": "^extern\\s+crate\\s+([a-zA-Z_][a-zA-Z0-9_]*)", 88 | "format": "extern crate {module}", 89 | "group": 1 90 | } 91 | ], 92 | "prompt_template": "Based on the following use statements from a Rust project, generate a Cargo.toml dependencies section.\n\nUse statements found:\n{imports}\n\nExisting Cargo.toml (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external crates (not std library)\n2. Reasonable version specifications\n3. Common crates with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the [dependencies] section of Cargo.toml." 93 | }, 94 | "csharp": { 95 | "dependency_files": ["*.csproj", "packages.config", "*.sln"], 96 | "file_extensions": ["*.cs"], 97 | "import_patterns": [ 98 | { 99 | "pattern": "^using\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*);", 100 | "format": "using {module}", 101 | "group": 1 102 | } 103 | ], 104 | "prompt_template": "Based on the following using statements from a C# project, generate NuGet package references.\n\nUsing statements found:\n{imports}\n\nExisting project file (if any):\n{existing}\n\nPlease generate package references that include:\n1. Only external packages (not System.* built-in namespaces)\n2. Reasonable version specifications\n3. Common packages with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn the PackageReference elements in XML format." 105 | }, 106 | "php": { 107 | "dependency_files": ["composer.json", "composer.lock"], 108 | "file_extensions": ["*.php"], 109 | "import_patterns": [ 110 | { 111 | "pattern": "^use\\s+([a-zA-Z_\\\\][a-zA-Z0-9_\\\\]*);", 112 | "format": "use {module}", 113 | "group": 1 114 | }, 115 | { 116 | "pattern": "^require_once\\s+['\"]([^'\"]+)['\"]", 117 | "format": "require_once '{module}'", 118 | "group": 1 119 | }, 120 | { 121 | "pattern": "^include_once\\s+['\"]([^'\"]+)['\"]", 122 | "format": "include_once '{module}'", 123 | "group": 1 124 | } 125 | ], 126 | "prompt_template": "Based on the following use/require statements from a PHP project, generate a composer.json dependencies section.\n\nStatements found:\n{imports}\n\nExisting composer.json (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external packages (not built-in PHP classes)\n2. Reasonable version specifications (use ^ for flexibility)\n3. Common packages with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the require section in JSON format." 127 | }, 128 | "ruby": { 129 | "dependency_files": ["Gemfile", "Gemfile.lock", "*.gemspec"], 130 | "file_extensions": ["*.rb"], 131 | "import_patterns": [ 132 | { 133 | "pattern": "^require\\s+['\"]([^'\"]+)['\"]", 134 | "format": "require '{module}'", 135 | "group": 1 136 | }, 137 | { 138 | "pattern": "^require_relative\\s+['\"]([^'\"]+)['\"]", 139 | "format": "require_relative '{module}'", 140 | "group": 1 141 | } 142 | ], 143 | "prompt_template": "Based on the following require statements from a Ruby project, generate a Gemfile.\n\nRequire statements found:\n{imports}\n\nExisting Gemfile (if any):\n{existing}\n\nPlease generate gem dependencies that include:\n1. Only external gems (not built-in Ruby libraries)\n2. Reasonable version specifications\n3. Common gems with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the Gemfile content." 144 | }, 145 | "matlab": { 146 | "dependency_files": ["requirements.txt", "toolbox_dependencies.txt", "*.prj"], 147 | "file_extensions": ["*.m", "*.mlx", "*.mlapp"], 148 | "import_patterns": [ 149 | { 150 | "pattern": "^addpath\\(['\"]([^'\"]+)['\"]\\)", 151 | "format": "addpath('{module}')", 152 | "group": 1 153 | }, 154 | { 155 | "pattern": "^import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", 156 | "format": "import {module}", 157 | "group": 1 158 | }, 159 | { 160 | "pattern": "^toolbox\\s*\\(['\"]([^'\"]+)['\"]\\)", 161 | "format": "toolbox('{module}')", 162 | "group": 1 163 | } 164 | ], 165 | "prompt_template": "Based on the following import/addpath statements from a MATLAB project, generate a toolbox dependencies file.\n\nStatements found:\n{imports}\n\nExisting dependencies (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external toolboxes and packages (not built-in MATLAB functions)\n2. Common MATLAB toolboxes with their names\n3. Third-party packages from File Exchange or GitHub\n4. Merge with existing dependencies if provided\n\nReturn only the dependencies list, one per line." 166 | }, 167 | "c": { 168 | "dependency_files": ["Makefile", "CMakeLists.txt", "configure.ac", "*.pc"], 169 | "file_extensions": ["*.c", "*.h"], 170 | "import_patterns": [ 171 | { 172 | "pattern": "^#include\\s*<([^>]+)>", 173 | "format": "#include <{module}>", 174 | "group": 1 175 | }, 176 | { 177 | "pattern": "^#include\\s*['\"]([^'\"]+)['\"]", 178 | "format": "#include \"{module}\"", 179 | "group": 1 180 | } 181 | ], 182 | "prompt_template": "Based on the following #include statements from a C project, generate build dependencies.\n\nInclude statements found:\n{imports}\n\nExisting build file (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external libraries (not standard C library headers)\n2. Common C libraries like OpenSSL, zlib, curl, etc.\n3. System libraries that need to be linked\n4. Merge with existing dependencies if provided\n\nReturn the dependencies in Makefile format or CMake format as appropriate." 183 | }, 184 | "cpp": { 185 | "dependency_files": ["Makefile", "CMakeLists.txt", "conanfile.txt", "vcpkg.json"], 186 | "file_extensions": ["*.cpp", "*.cxx", "*.cc", "*.hpp", "*.hxx", "*.h"], 187 | "import_patterns": [ 188 | { 189 | "pattern": "^#include\\s*<([^>]+)>", 190 | "format": "#include <{module}>", 191 | "group": 1 192 | }, 193 | { 194 | "pattern": "^#include\\s*['\"]([^'\"]+)['\"]", 195 | "format": "#include \"{module}\"", 196 | "group": 1 197 | } 198 | ], 199 | "prompt_template": "Based on the following #include statements from a C++ project, generate build dependencies.\n\nInclude statements found:\n{imports}\n\nExisting build file (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external libraries (not standard C++ library headers)\n2. Common C++ libraries like Boost, Qt, OpenCV, etc.\n3. System libraries that need to be linked\n4. Merge with existing dependencies if provided\n\nReturn the dependencies in CMake format or Conan format as appropriate." 200 | } 201 | }, 202 | "default_language": "python", 203 | "builtin_modules": { 204 | "python": [ 205 | "os", "sys", "re", "json", "urllib", "http", "datetime", "time", "math", 206 | "random", "collections", "itertools", "functools", "operator", "pathlib", 207 | "typing", "abc", "contextlib", "copy", "pickle", "sqlite3", "csv", 208 | "configparser", "logging", "unittest", "threading", "multiprocessing", 209 | "asyncio", "socket", "email", "html", "xml", "base64", "hashlib", 210 | "hmac", "secrets", "uuid", "decimal", "fractions", "statistics", 211 | "io", "tempfile", "shutil", "glob", "fnmatch", "linecache", "fileinput" 212 | ], 213 | "javascript": [ 214 | "fs", "path", "os", "crypto", "util", "events", "stream", "buffer", 215 | "url", "querystring", "http", "https", "net", "dns", "cluster", 216 | "child_process", "readline", "repl", "vm", "assert", "console" 217 | ], 218 | "java": [ 219 | "java.lang", "java.util", "java.io", "java.net", "java.nio", "java.text", 220 | "java.time", "java.math", "java.security", "java.sql", "javax.sql", 221 | "javax.swing", "javax.awt", "javax.crypto", "javax.xml" 222 | ], 223 | "go": [ 224 | "fmt", "os", "io", "net", "http", "time", "strings", "strconv", "math", 225 | "sort", "sync", "context", "encoding", "crypto", "database", "log", 226 | "flag", "path", "regexp", "runtime", "testing", "unsafe", "reflect" 227 | ], 228 | "rust": [ 229 | "std", "core", "alloc", "proc_macro", "test" 230 | ], 231 | "csharp": [ 232 | "System", "System.Collections", "System.IO", "System.Net", "System.Text", 233 | "System.Threading", "System.Linq", "System.Data", "System.Xml", 234 | "System.Drawing", "System.Windows", "Microsoft" 235 | ], 236 | "php": [ 237 | "DateTime", "PDO", "Exception", "stdClass", "Closure", "Generator", 238 | "ReflectionClass", "SplFileInfo", "ArrayIterator", "RecursiveDirectoryIterator" 239 | ], 240 | "ruby": [ 241 | "json", "yaml", "csv", "uri", "net", "open", "time", "date", "digest", 242 | "base64", "fileutils", "pathname", "tempfile", "logger", "benchmark" 243 | ], 244 | "matlab": [ 245 | "abs", "acos", "asin", "atan", "atan2", "ceil", "cos", "exp", "floor", 246 | "log", "log10", "max", "min", "mod", "rand", "randn", "round", "sin", 247 | "sqrt", "tan", "zeros", "ones", "eye", "size", "length", "numel", 248 | "find", "sort", "unique", "sum", "mean", "std", "var", "plot", "figure", 249 | "subplot", "xlabel", "ylabel", "title", "legend", "grid", "hold", 250 | "disp", "fprintf", "sprintf", "input", "load", "save", "clear", "clc", 251 | "who", "whos", "exist", "which", "path", "cd", "pwd", "dir", "ls", 252 | "mkdir", "rmdir", "delete", "copyfile", "movefile", "isdir", "isfile" 253 | ], 254 | "c": [ 255 | "stdio.h", "stdlib.h", "string.h", "math.h", "time.h", "ctype.h", 256 | "assert.h", "errno.h", "float.h", "limits.h", "locale.h", "setjmp.h", 257 | "signal.h", "stdarg.h", "stddef.h", "unistd.h", "sys/types.h", 258 | "sys/stat.h", "fcntl.h", "dirent.h", "pthread.h" 259 | ], 260 | "cpp": [ 261 | "iostream", "vector", "string", "algorithm", "memory", "utility", 262 | "functional", "iterator", "numeric", "map", "set", "unordered_map", 263 | "unordered_set", "queue", "stack", "deque", "list", "array", "tuple", 264 | "chrono", "thread", "mutex", "condition_variable", "future", "atomic", 265 | "regex", "random", "typeinfo", "exception", "stdexcept", "cassert", 266 | "cmath", "cstdlib", "cstring", "ctime", "cctype", "climits", "cfloat" 267 | ] 268 | } 269 | } -------------------------------------------------------------------------------- /src/readmex/utils/language_analyzer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from collections import defaultdict 4 | from pathlib import Path 5 | from typing import Dict, List, Any, Optional 6 | import fnmatch 7 | 8 | 9 | class LanguageAnalyzer: 10 | """Project Language Analyzer - Detect programming language distribution in projects""" 11 | 12 | def __init__(self, config_path: Optional[str] = None): 13 | """ 14 | Initialize language analyzer 15 | 16 | Args: 17 | config_path: Language mapping config file path, if None use default path 18 | """ 19 | self.language_mapping = self._load_language_mapping(config_path) 20 | self.extension_to_language = self._build_extension_mapping() 21 | 22 | # Load ignore patterns 23 | self.ignore_dirs, self.ignore_files = self._load_ignore_patterns() 24 | 25 | def _load_language_mapping(self, config_path: Optional[str] = None) -> Dict[str, List[str]]: 26 | """Load language mapping config""" 27 | if config_path is None: 28 | # Use default path 29 | current_dir = Path(__file__).parent.parent 30 | config_path = current_dir / "config" / "language_mapping.json" 31 | 32 | try: 33 | with open(config_path, 'r', encoding='utf-8') as f: 34 | return json.load(f) 35 | except FileNotFoundError: 36 | print(f"Warning: Language mapping config file {config_path} not found, using default config") 37 | return self._get_default_mapping() 38 | except json.JSONDecodeError as e: 39 | print(f"Warning: Language mapping config file format error: {e}, using default config") 40 | return self._get_default_mapping() 41 | 42 | def _get_default_mapping(self) -> Dict[str, List[str]]: 43 | """Get default language mapping""" 44 | return { 45 | "Python": [".py", ".pyx", ".pyi", ".pyw"], 46 | "JavaScript": [".js", ".jsx", ".mjs"], 47 | "TypeScript": [".ts", ".tsx"], 48 | "Java": [".java", ".class", ".jar"], 49 | "C": [".c"], 50 | "C++": [".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".h++"], 51 | "C/C++": [".h"], 52 | "Go": [".go"], 53 | "Rust": [".rs"], 54 | "PHP": [".php", ".phtml"], 55 | "Ruby": [".rb", ".erb", ".rake"], 56 | "HTML": [".html", ".htm"], 57 | "CSS": [".css"], 58 | "JSON": [".json"], 59 | "Markdown": [".md", ".markdown"], 60 | "Text": [".txt"], 61 | "Shell": [".sh", ".bash", ".zsh", ".fish"], 62 | "SQL": [".sql"], 63 | "YAML": [".yaml", ".yml"], 64 | "XML": [".xml"] 65 | } 66 | 67 | def _build_extension_mapping(self) -> Dict[str, str]: 68 | """Build extension to language mapping""" 69 | extension_map = {} 70 | for language, extensions in self.language_mapping.items(): 71 | for ext in extensions: 72 | # Handle special filenames (e.g. Dockerfile, Makefile) 73 | if not ext.startswith('.'): 74 | extension_map[ext] = language 75 | else: 76 | extension_map[ext.lower()] = language 77 | return extension_map 78 | 79 | def _load_ignore_patterns(self) -> tuple[set, set]: 80 | """Load ignore patterns config""" 81 | # Use default path 82 | current_dir = Path(__file__).parent.parent 83 | config_path = current_dir / "config" / "ignore_patterns.json" 84 | 85 | try: 86 | with open(config_path, 'r', encoding='utf-8') as f: 87 | config = json.load(f) 88 | ignore_dirs = set(config.get('ignore_dirs', [])) 89 | ignore_files = set(config.get('ignore_files', [])) 90 | return ignore_dirs, ignore_files 91 | except FileNotFoundError: 92 | print(f"Warning: Ignore patterns config file {config_path} not found, using default config") 93 | return self._get_default_ignore_patterns() 94 | except json.JSONDecodeError as e: 95 | print(f"Warning: Ignore patterns config file format error: {e}, using default config") 96 | return self._get_default_ignore_patterns() 97 | 98 | def _get_default_ignore_patterns(self) -> tuple[set, set]: 99 | """Get default ignore patterns""" 100 | ignore_dirs = { 101 | '.git', '__pycache__', 'node_modules', '.venv', 'venv', 102 | 'env', '.pytest_cache', '.idea', '.vscode', 'build', 103 | 'dist', 'target', 'bin', 'obj', '.DS_Store', '.mypy_cache', 104 | 'coverage', '.coverage', 'htmlcov', '.tox', '.eggs', 105 | '*.egg-info', '*.egg', '.pytest_cache', '.cache', 106 | '.next', '.nuxt', 'out', '.output', '.vercel' 107 | } 108 | 109 | ignore_files = { 110 | '.DS_Store', '.gitignore', '.gitattributes', '.editorconfig', 111 | 'Thumbs.db', 'desktop.ini', '.env', '.env.local', '.env.production' 112 | } 113 | 114 | return ignore_dirs, ignore_files 115 | 116 | def analyze_project(self, project_path: str) -> Dict[str, Any]: 117 | """ 118 | Analyze language distribution in the project 119 | 120 | Args: 121 | project_path: Project path 122 | 123 | Returns: 124 | Dictionary containing language analysis results 125 | """ 126 | project_path = Path(project_path) 127 | if not project_path.exists(): 128 | raise FileNotFoundError(f"Project path does not exist: {project_path}") 129 | 130 | language_stats = defaultdict(lambda: {'files': 0, 'lines': 0, 'bytes': 0}) 131 | 132 | # Traverse all files in the project 133 | for file_path in project_path.rglob('*'): 134 | if file_path.is_file(): 135 | # Check if the file should be ignored 136 | if self._should_ignore(file_path): 137 | continue 138 | 139 | # Determine language type 140 | language = self._get_language(file_path) 141 | 142 | if language: 143 | # Count file info 144 | try: 145 | file_size = file_path.stat().st_size 146 | line_count = self._count_lines(file_path) 147 | 148 | language_stats[language]['files'] += 1 149 | language_stats[language]['lines'] += line_count 150 | language_stats[language]['bytes'] += file_size 151 | 152 | except (OSError, UnicodeDecodeError): 153 | # Skip unreadable files 154 | continue 155 | 156 | return self._calculate_percentages(language_stats) 157 | 158 | def _should_ignore(self, file_path: Path) -> bool: 159 | """Check if the file or directory should be ignored""" 160 | # Check directory names 161 | for part in file_path.parts: 162 | if part in self.ignore_dirs: 163 | return True 164 | # Check wildcard patterns 165 | for pattern in self.ignore_dirs: 166 | if '*' in pattern and fnmatch.fnmatch(part, pattern): 167 | return True 168 | 169 | # Check file names 170 | if file_path.name in self.ignore_files: 171 | return True 172 | 173 | return False 174 | 175 | def _get_language(self, file_path: Path) -> Optional[str]: 176 | """Determine language type by file extension and content""" 177 | # Check special filenames (e.g. Dockerfile, Makefile) 178 | if file_path.name in self.extension_to_language: 179 | return self.extension_to_language[file_path.name] 180 | 181 | # Check file extension 182 | extension = file_path.suffix.lower() 183 | if extension in self.extension_to_language: 184 | return self.extension_to_language[extension] 185 | 186 | # Check shebang line for script language 187 | try: 188 | with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: 189 | first_line = f.readline().strip() 190 | if first_line.startswith('#!'): 191 | return self._detect_shebang_language(first_line) 192 | except: 193 | pass 194 | 195 | return None 196 | 197 | def _detect_shebang_language(self, shebang_line: str) -> str: 198 | """Detect language by shebang line""" 199 | shebang = shebang_line.lower() 200 | 201 | if 'python' in shebang: 202 | return 'Python' 203 | elif 'bash' in shebang or 'sh' in shebang: 204 | return 'Shell' 205 | elif 'node' in shebang: 206 | return 'JavaScript' 207 | elif 'ruby' in shebang: 208 | return 'Ruby' 209 | elif 'perl' in shebang: 210 | return 'Perl' 211 | elif 'php' in shebang: 212 | return 'PHP' 213 | 214 | return 'Shell' # Default to Shell 215 | 216 | def _count_lines(self, file_path: Path) -> int: 217 | """Count file lines""" 218 | try: 219 | with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: 220 | return sum(1 for _ in f) 221 | except: 222 | return 0 223 | 224 | def _calculate_percentages(self, language_stats: Dict) -> Dict[str, Any]: 225 | """Calculate percentages for each language""" 226 | total_files = sum(stats['files'] for stats in language_stats.values()) 227 | total_lines = sum(stats['lines'] for stats in language_stats.values()) 228 | total_bytes = sum(stats['bytes'] for stats in language_stats.values()) 229 | 230 | results = [] 231 | for language, stats in language_stats.items(): 232 | if total_files > 0: 233 | file_percentage = (stats['files'] / total_files) * 100 234 | else: 235 | file_percentage = 0 236 | 237 | if total_lines > 0: 238 | line_percentage = (stats['lines'] / total_lines) * 100 239 | else: 240 | line_percentage = 0 241 | 242 | if total_bytes > 0: 243 | byte_percentage = (stats['bytes'] / total_bytes) * 100 244 | else: 245 | byte_percentage = 0 246 | 247 | results.append({ 248 | 'language': language, 249 | 'files': stats['files'], 250 | 'lines': stats['lines'], 251 | 'bytes': stats['bytes'], 252 | 'file_percentage': round(file_percentage, 2), 253 | 'line_percentage': round(line_percentage, 2), 254 | 'byte_percentage': round(byte_percentage, 2) 255 | }) 256 | 257 | # Sort by lines of code 258 | results.sort(key=lambda x: x['lines'], reverse=True) 259 | 260 | return { 261 | 'summary': { 262 | 'total_files': total_files, 263 | 'total_lines': total_lines, 264 | 'total_bytes': total_bytes, 265 | 'total_languages': len(results) 266 | }, 267 | 'languages': results 268 | } 269 | 270 | def get_primary_language(self, project_path: str) -> Optional[str]: 271 | """ 272 | Get the primary programming language of the project 273 | 274 | Args: 275 | project_path: Project path 276 | 277 | Returns: 278 | Primary programming language name, or None if not found 279 | """ 280 | results = self.analyze_project(project_path) 281 | if results['languages']: 282 | return results['languages'][0]['language'] 283 | return None 284 | 285 | def get_language_summary(self, project_path: str) -> str: 286 | """ 287 | Get a text summary of the project's language distribution 288 | 289 | Args: 290 | project_path: Project path 291 | 292 | Returns: 293 | Language distribution summary text 294 | """ 295 | results = self.analyze_project(project_path) 296 | 297 | if not results['languages']: 298 | return "No programming language files detected." 299 | 300 | summary_lines = [] 301 | summary_lines.append(f"Project contains {results['summary']['total_languages']} programming languages") 302 | summary_lines.append(f"Total files: {results['summary']['total_files']}") 303 | summary_lines.append(f"Total lines of code: {results['summary']['total_lines']}") 304 | 305 | # Show top 3 main languages 306 | top_languages = results['languages'][:3] 307 | summary_lines.append("\nMain languages:") 308 | for lang in top_languages: 309 | summary_lines.append( 310 | f" {lang['language']}: {lang['line_percentage']:.1f}% " 311 | f"({lang['lines']} lines, {lang['files']} files)" 312 | ) 313 | 314 | return "\n".join(summary_lines) 315 | 316 | def save_analysis_result(self, project_path: str, output_path: str) -> None: 317 | """ 318 | Save language analysis result to file 319 | 320 | Args: 321 | project_path: Project path 322 | output_path: Output file path 323 | """ 324 | results = self.analyze_project(project_path) 325 | 326 | with open(output_path, 'w', encoding='utf-8') as f: 327 | json.dump(results, f, ensure_ascii=False, indent=2) 328 | 329 | 330 | def analyze_project_languages(project_path: str, config_path: Optional[str] = None) -> Dict[str, Any]: 331 | """ 332 | Convenience function: analyze project language distribution 333 | 334 | Args: 335 | project_path: Project path 336 | config_path: Language mapping config file path 337 | 338 | Returns: 339 | Language analysis result 340 | """ 341 | analyzer = LanguageAnalyzer(config_path) 342 | return analyzer.analyze_project(project_path) 343 | 344 | if __name__ == "__main__": 345 | """Test when running this script directly""" 346 | import sys 347 | from pathlib import Path 348 | 349 | # Get the project root directory as the test path 350 | current_dir = Path(__file__).parent.parent.parent 351 | 352 | print("🔍 Language Analyzer Test") 353 | print("=" * 50) 354 | print(f"Test project path: {current_dir}") 355 | print() 356 | 357 | try: 358 | # Create analyzer instance 359 | analyzer = LanguageAnalyzer() 360 | 361 | # Analyze project 362 | print("Analyzing project...") 363 | results = analyzer.analyze_project(str(current_dir)) 364 | 365 | # Show results 366 | print("\n📊 Analysis Result:") 367 | print(f" Total files: {results['summary']['total_files']}") 368 | print(f" Total languages: {results['summary']['total_languages']}") 369 | print(f" Total lines of code: {results['summary']['total_lines']}") 370 | print(f" Total file size: {results['summary']['total_bytes']} bytes") 371 | 372 | if results['languages']: 373 | print("\n📈 Language Distribution:") 374 | print("-" * 60) 375 | print(f"{'Language':<15} {'Files':<8} {'Lines':<10} {'File %':<10} {'Line %':<10}") 376 | print("-" * 60) 377 | 378 | for lang in results['languages'][:10]: # Show top 10 languages 379 | print(f"{lang['language']:<15} {lang['files']:<8} {lang['lines']:<10} " 380 | f"{lang['file_percentage']:<10.1f}% {lang['line_percentage']:<10.1f}%") 381 | 382 | print("-" * 60) 383 | 384 | # Get primary language 385 | primary_lang = analyzer.get_primary_language(str(current_dir)) 386 | print(f"\n🏆 Primary Language: {primary_lang}") 387 | 388 | # Get language summary 389 | summary = analyzer.get_language_summary(str(current_dir)) 390 | print(f"\n📋 Language Summary:\n{summary}") 391 | 392 | else: 393 | print("\n⚠️ No programming language files detected.") 394 | 395 | print("\n✅ Test completed!") 396 | 397 | except FileNotFoundError as e: 398 | print(f"❌ Error: Project path does not exist - {e}") 399 | sys.exit(1) 400 | except Exception as e: 401 | print(f"❌ Error: {e}") 402 | sys.exit(1) 403 | -------------------------------------------------------------------------------- /src/readmex/utils/dependency_analyzer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import json 4 | from typing import Set, List, Dict, Any 5 | from pathlib import Path 6 | from rich.console import Console 7 | from readmex.utils.file_handler import find_files, load_gitignore_patterns 8 | from readmex.config import DEFAULT_IGNORE_PATTERNS 9 | from readmex.utils.model_client import ModelClient 10 | 11 | 12 | class DependencyAnalyzer: 13 | """Multi-language project dependency analyzer class""" 14 | 15 | def __init__(self, project_dir: str, primary_language: str = "python", model_client=None, console=None): 16 | """ 17 | Initialize dependency analyzer 18 | 19 | Args: 20 | project_dir: Project root directory path 21 | primary_language: Primary programming language of the project 22 | model_client: Model client for generating dependency files 23 | console: Rich console object for output 24 | """ 25 | self.project_dir = project_dir 26 | # 安全处理可能为 None 的 primary_language 参数 27 | self.primary_language = (primary_language or "python").lower() 28 | self.model_client = model_client 29 | self.console = console or Console() 30 | 31 | # Load dependency configuration 32 | self.config = self._load_dependency_config() 33 | 34 | # Validate primary language 35 | if self.primary_language not in self.config["languages"]: 36 | self.console.print(f"[yellow]Warning: Language '{self.primary_language}' not supported, falling back to default[/yellow]") 37 | self.primary_language = self.config["default_language"] 38 | 39 | def _load_dependency_config(self) -> Dict[str, Any]: 40 | """Load dependency configuration from JSON file""" 41 | try: 42 | config_path = Path(__file__).parent.parent / "config" / "dependency_config.json" 43 | with open(config_path, 'r', encoding='utf-8') as f: 44 | return json.load(f) 45 | except FileNotFoundError: 46 | self.console.print("[red]Warning: dependency_config.json not found, using default Python configuration[/red]") 47 | return self._get_default_config() 48 | except json.JSONDecodeError as e: 49 | self.console.print(f"[red]Warning: Invalid JSON in dependency_config.json: {e}[/red]") 50 | return self._get_default_config() 51 | 52 | def _get_default_config(self) -> Dict[str, Any]: 53 | """Get default configuration for Python""" 54 | return { 55 | "languages": { 56 | "python": { 57 | "dependency_files": ["requirements.txt"], 58 | "file_extensions": ["*.py"], 59 | "import_patterns": [ 60 | { 61 | "pattern": "^import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", 62 | "format": "import {module}", 63 | "group": 1 64 | }, 65 | { 66 | "pattern": "^from\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\s+import\\s+(.+)", 67 | "format": "from {module} import {items}", 68 | "group": 1, 69 | "items_group": 2 70 | } 71 | ], 72 | "prompt_template": "Based on the following import statements from a Python project, generate a requirements.txt file with appropriate package versions.\n\nImport statements found:\n{imports}\n\nExisting requirements.txt (if any):\n{existing}\n\nPlease generate a complete requirements.txt file that includes:\n1. Only external packages (not built-in Python modules)\n2. Reasonable version specifications (use >= for flexibility)\n3. Common packages with their typical versions\n4. Merge with existing requirements if provided\n\nReturn only the requirements.txt content, one package per line in format: package>=version" 73 | } 74 | }, 75 | "default_language": "python", 76 | "builtin_modules": { 77 | "python": ["os", "sys", "re", "json", "urllib", "http", "datetime", "time", "math", "random", "collections", "itertools", "functools", "operator", "pathlib", "typing", "abc", "contextlib", "copy", "pickle", "sqlite3", "csv", "configparser", "logging", "unittest", "threading", "multiprocessing", "asyncio", "socket", "email", "html", "xml", "base64", "hashlib", "hmac", "secrets", "uuid", "decimal", "fractions", "statistics", "io", "tempfile", "shutil", "glob", "fnmatch", "linecache", "fileinput"] 78 | } 79 | } 80 | 81 | def analyze_project_dependencies(self, output_dir: str = None) -> str: 82 | """ 83 | Analyze project dependencies and generate dependency files 84 | 85 | Args: 86 | output_dir: Output directory, saves files if provided 87 | 88 | Returns: 89 | Generated dependency file content 90 | """ 91 | lang_config = self.config["languages"][self.primary_language] 92 | self.console.print(f"[cyan]🤖 Generating {self.primary_language} project dependencies...[/cyan]") 93 | 94 | # Check if existing dependency files exist 95 | existing_dependencies = self._get_existing_dependencies() 96 | if existing_dependencies: 97 | self.console.print(f"[yellow]Found existing dependency files[/yellow]") 98 | 99 | # Scan all source files to extract import statements 100 | gitignore_patterns = load_gitignore_patterns(self.project_dir) 101 | ignore_patterns = DEFAULT_IGNORE_PATTERNS + gitignore_patterns 102 | source_files = list(find_files(self.project_dir, lang_config["file_extensions"], ignore_patterns)) 103 | 104 | all_imports = set() 105 | 106 | if source_files: 107 | self.console.print(f"Scanning {len(source_files)} {self.primary_language} files for imports...") 108 | 109 | for source_file in source_files: 110 | try: 111 | with open(source_file, "r", encoding="utf-8") as f: 112 | content = f.read() 113 | 114 | # Extract import statements using language-specific patterns 115 | import_lines = self._extract_imports_by_language(content) 116 | all_imports.update(import_lines) 117 | 118 | except Exception as e: 119 | self.console.print( 120 | f"[yellow]Warning: Could not read {source_file}: {e}[/yellow]" 121 | ) 122 | 123 | if all_imports: 124 | self.console.print(f"Found {len(all_imports)} unique import statements") 125 | 126 | # Filter out built-in modules 127 | external_imports = self._filter_external_imports(all_imports) 128 | 129 | if external_imports: 130 | # Use LLM to generate dependency file 131 | imports_text = "\n".join(sorted(external_imports)) 132 | prompt = lang_config["prompt_template"].format( 133 | imports=imports_text, 134 | existing=existing_dependencies 135 | ) 136 | 137 | self.console.print(f"Generating {self.primary_language} dependency file...") 138 | generated_dependencies = self.model_client.get_answer(prompt) 139 | 140 | # Clean the generated content 141 | generated_dependencies = self._clean_dependency_content( 142 | generated_dependencies 143 | ) 144 | else: 145 | generated_dependencies = f"# No external {self.primary_language} dependencies found\n" 146 | if existing_dependencies: 147 | generated_dependencies = existing_dependencies 148 | else: 149 | generated_dependencies = f"# No {self.primary_language} import statements found\n" 150 | if existing_dependencies: 151 | generated_dependencies = existing_dependencies 152 | else: 153 | generated_dependencies = f"# No {self.primary_language} files found\n" 154 | if existing_dependencies: 155 | generated_dependencies = existing_dependencies 156 | 157 | self.console.print(f"Generated dependencies: {generated_dependencies}") 158 | # Save generated dependency files to output folder 159 | if output_dir: 160 | self._save_dependency_files( 161 | output_dir, generated_dependencies, existing_dependencies, all_imports 162 | ) 163 | 164 | self.console.print(f"[green]✔ {self.primary_language} project dependencies generated.[/green]") 165 | return generated_dependencies 166 | 167 | def _get_existing_dependencies(self) -> str: 168 | """Get existing dependency file content for the current language""" 169 | lang_config = self.config["languages"][self.primary_language] 170 | existing_content = "" 171 | 172 | for dep_file in lang_config["dependency_files"]: 173 | dep_path = os.path.join(self.project_dir, dep_file) 174 | if os.path.exists(dep_path): 175 | try: 176 | with open(dep_path, "r", encoding="utf-8") as f: 177 | content = f.read() 178 | if content.strip(): 179 | existing_content += f"\n=== {dep_file} ===\n{content}\n" 180 | except Exception as e: 181 | self.console.print(f"[yellow]Warning: Could not read {dep_file}: {e}[/yellow]") 182 | 183 | return existing_content.strip() 184 | 185 | def _extract_imports_by_language(self, content: str) -> Set[str]: 186 | """Extract import statements using language-specific patterns""" 187 | lang_config = self.config["languages"][self.primary_language] 188 | imports = set() 189 | lines = content.split("\n") 190 | 191 | for line in lines: 192 | line = line.strip() 193 | 194 | # Skip comment lines and empty lines 195 | if not line or line.startswith("#") or line.startswith("//") or line.startswith("/*"): 196 | continue 197 | 198 | # Apply language-specific patterns 199 | for pattern_config in lang_config["import_patterns"]: 200 | pattern = pattern_config["pattern"] 201 | match = re.match(pattern, line) 202 | 203 | if match: 204 | module = match.group(pattern_config["group"]) 205 | 206 | # Format the import statement 207 | if "items_group" in pattern_config: 208 | items = match.group(pattern_config["items_group"]) 209 | formatted = pattern_config["format"].format(module=module, items=items) 210 | else: 211 | formatted = pattern_config["format"].format(module=module) 212 | 213 | imports.add(formatted) 214 | break 215 | 216 | return imports 217 | 218 | def _filter_external_imports(self, imports: Set[str]) -> Set[str]: 219 | """Filter out built-in modules to keep only external dependencies""" 220 | builtin_modules = self.config.get("builtin_modules", {}).get(self.primary_language, []) 221 | external_imports = set() 222 | 223 | for import_stmt in imports: 224 | # Extract module name from import statement 225 | module_name = self._extract_module_name(import_stmt) 226 | 227 | # Check if it's a built-in module 228 | is_builtin = False 229 | for builtin in builtin_modules: 230 | if module_name.startswith(builtin): 231 | is_builtin = True 232 | break 233 | 234 | # Skip relative imports and local files 235 | if not is_builtin and not module_name.startswith('.') and '/' not in module_name: 236 | external_imports.add(import_stmt) 237 | 238 | return external_imports 239 | 240 | def _extract_module_name(self, import_stmt: str) -> str: 241 | """Extract the base module name from an import statement""" 242 | # Handle different import formats 243 | if import_stmt.startswith("import "): 244 | module = import_stmt[7:].split()[0] 245 | elif import_stmt.startswith("from "): 246 | module = import_stmt.split(" import ")[0][5:] 247 | elif "require(" in import_stmt: 248 | # JavaScript/Node.js require 249 | match = re.search(r"require\(['\"]([^'\"]+)['\"]", import_stmt) 250 | module = match.group(1) if match else "" 251 | elif import_stmt.startswith("use "): 252 | # Rust use statement 253 | module = import_stmt[4:].split("::")[0] 254 | elif import_stmt.startswith("using "): 255 | # C# using statement 256 | module = import_stmt[6:].rstrip(";") 257 | else: 258 | module = import_stmt 259 | 260 | # Get the top-level module name 261 | return module.split('.')[0].split('::')[0].split('/')[0] 262 | 263 | def _clean_dependency_content(self, content: str) -> str: 264 | """Clean generated dependency file content (language-agnostic)""" 265 | lines = content.split("\n") 266 | cleaned_lines = [] 267 | 268 | for line in lines: 269 | line = line.strip() 270 | 271 | # Skip empty lines and obvious non-dependency format lines 272 | if not line or line.startswith("```") or line.startswith("Based on"): 273 | continue 274 | 275 | # Keep lines that look like dependency specifications 276 | if any(char in line for char in ["=", ">", "<", "~", "^"]) or line.startswith("#"): 277 | cleaned_lines.append(line) 278 | elif re.match(r"^[a-zA-Z0-9_-]+$", line): 279 | # If only package name, keep as is (version will be handled by LLM) 280 | cleaned_lines.append(line) 281 | 282 | return "\n".join(cleaned_lines) 283 | 284 | def _save_dependency_files( 285 | self, 286 | output_dir: str, 287 | generated_dependencies: str, 288 | existing_dependencies: str, 289 | all_imports: Set[str] 290 | ) -> None: 291 | """Save dependency files and analysis information""" 292 | lang_config = self.config["languages"][self.primary_language] 293 | 294 | # Determine output filename based on language 295 | primary_dep_file = lang_config["dependency_files"][0] 296 | output_dep_path = os.path.join(output_dir, primary_dep_file) 297 | 298 | # Save generated dependency file 299 | with open(output_dep_path, "w", encoding="utf-8") as f: 300 | f.write(generated_dependencies) 301 | self.console.print( 302 | f"[green]✔ Generated {primary_dep_file} saved to: {output_dep_path}[/green]" 303 | ) 304 | 305 | # Save dependency analysis information 306 | dependencies_info = f"""# {self.primary_language.title()} Dependencies Analysis Report 307 | 308 | ## Language: {self.primary_language} 309 | ## Primary dependency file: {primary_dep_file} 310 | 311 | ## Existing dependency files: 312 | {existing_dependencies if existing_dependencies else "None found"} 313 | 314 | ## Discovered imports ({len(all_imports)} unique): 315 | {chr(10).join(sorted(all_imports)) if all_imports else "No imports found"} 316 | 317 | ## Generated dependency file: 318 | {generated_dependencies} 319 | """ 320 | dependencies_analysis_path = os.path.join( 321 | output_dir, f"{self.primary_language}_dependencies_analysis.txt" 322 | ) 323 | with open(dependencies_analysis_path, "w", encoding="utf-8") as f: 324 | f.write(dependencies_info) 325 | self.console.print( 326 | f"[green]✔ Dependencies analysis saved to: {dependencies_analysis_path}[/green]" 327 | ) 328 | 329 | # Backward compatibility methods (deprecated) 330 | def _extract_imports(self, content: str) -> Set[str]: 331 | """ 332 | Legacy method for Python import extraction (deprecated) 333 | Use _extract_imports_by_language instead 334 | """ 335 | return self._extract_imports_by_language(content) 336 | 337 | def _clean_requirements_content(self, content: str) -> str: 338 | """ 339 | Legacy method for cleaning requirements (deprecated) 340 | Use _clean_dependency_content instead 341 | """ 342 | return self._clean_dependency_content(content) 343 | 344 | def _save_requirements_files(self, output_dir: str, generated_requirements: str, 345 | existing_dependencies: str, all_imports: Set[str]) -> None: 346 | """ 347 | Legacy method for saving requirements (deprecated) 348 | Use _save_dependency_files instead 349 | """ 350 | self._save_dependency_files(output_dir, generated_requirements, existing_dependencies, all_imports) 351 | 352 | def get_project_imports(self) -> Set[str]: 353 | """ 354 | Get all import statements in the project for the current language 355 | 356 | Returns: 357 | Set of all import statements 358 | """ 359 | lang_config = self.config["languages"][self.primary_language] 360 | gitignore_patterns = load_gitignore_patterns(self.project_dir) 361 | ignore_patterns = DEFAULT_IGNORE_PATTERNS + gitignore_patterns 362 | source_files = list(find_files(self.project_dir, lang_config["file_extensions"], ignore_patterns)) 363 | 364 | all_imports = set() 365 | 366 | for source_file in source_files: 367 | try: 368 | with open(source_file, "r", encoding="utf-8") as f: 369 | content = f.read() 370 | import_lines = self._extract_imports_by_language(content) 371 | all_imports.update(import_lines) 372 | except Exception: 373 | continue 374 | 375 | return all_imports 376 | 377 | def get_existing_requirements(self) -> str: 378 | """ 379 | Get existing dependency file content for the current language 380 | 381 | Returns: 382 | Existing dependency file content, or empty string if not found 383 | """ 384 | return self._get_existing_dependencies() 385 | 386 | def get_supported_languages(self) -> List[str]: 387 | """ 388 | Get list of supported programming languages 389 | 390 | Returns: 391 | List of supported language names 392 | """ 393 | return list(self.config["languages"].keys()) 394 | 395 | def set_language(self, language: str) -> bool: 396 | """ 397 | Change the primary language for dependency analysis 398 | 399 | Args: 400 | language: New primary language 401 | 402 | Returns: 403 | True if language was set successfully, False if not supported 404 | """ 405 | # 安全处理可能为 None 的 language 参数 406 | if language is None: 407 | self.console.print(f"[red]Language cannot be None[/red]") 408 | return False 409 | 410 | language = language.lower() 411 | if language in self.config["languages"]: 412 | self.primary_language = language 413 | self.console.print(f"[green]Language changed to: {language}[/green]") 414 | return True 415 | else: 416 | self.console.print(f"[red]Language '{language}' not supported[/red]") 417 | return False 418 | 419 | if __name__ == "__main__": 420 | from pathlib import Path 421 | 422 | # Test the multi-language dependency analyzer 423 | output_dir = Path(__file__).parent.parent.parent.parent / "readmex_output" 424 | output_dir.mkdir(exist_ok=True) 425 | 426 | # Test with Python (default) 427 | print("Testing Python dependency analysis...") 428 | model_client = ModelClient() 429 | analyzer = DependencyAnalyzer( 430 | project_dir=".", 431 | primary_language="python", 432 | model_client=model_client, 433 | console=None 434 | ) 435 | 436 | print(f"Supported languages: {analyzer.get_supported_languages()}") 437 | print(f"Current language: {analyzer.primary_language}") 438 | 439 | # Test import extraction 440 | imports = analyzer.get_project_imports() 441 | print(f"Found {len(imports)} import statements") 442 | 443 | # Test dependency analysis (commented out to avoid API calls) 444 | # analyzer.analyze_project_dependencies(output_dir=output_dir) 445 | 446 | # Test language switching 447 | print("\nTesting language switching...") 448 | analyzer.set_language("javascript") 449 | analyzer.set_language("unsupported_language") 450 | analyzer.set_language("python") -------------------------------------------------------------------------------- /src/readmex/utils/model_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | from rich.console import Console 3 | import requests 4 | from openai import OpenAI, AzureOpenAI 5 | from typing import Optional, Dict, Union 6 | from readmex.config import get_llm_config, get_t2i_config, validate_config 7 | import time 8 | 9 | 10 | class ModelClient: 11 | """Model client class for LLM Q&A and text-to-image functionality""" 12 | 13 | def __init__(self, max_tokens: int = 10000, temperature: float = 0.7, 14 | image_size: str = "1024x1024", quality: str = "hd"): 15 | """ 16 | Initialize model client 17 | 18 | Args: 19 | max_tokens: Maximum number of tokens 20 | temperature: Temperature parameter 21 | image_size: Image size 22 | quality: Image quality 23 | """ 24 | # Validate configuration 25 | validate_config() 26 | 27 | # Get configurations 28 | self.llm_config = get_llm_config() 29 | self.t2i_config = get_t2i_config() 30 | 31 | # Set parameters 32 | self.max_tokens = max_tokens 33 | self.temperature = temperature 34 | self.image_size = image_size 35 | self.quality = quality 36 | 37 | # Initialize console 38 | self.console = Console() 39 | 40 | # Check if we're using Azure OpenAI with detailed logging 41 | self.console.print("[cyan]🔧 ModelClient Initialization[/cyan]") 42 | self.console.print(f"[dim]LLM Base URL: {self.llm_config['base_url']}[/dim]") 43 | self.console.print(f"[dim]T2I Base URL: {self.t2i_config['base_url']}[/dim]") 44 | 45 | self.is_llm_azure = self._is_azure_openai(self.llm_config["base_url"]) 46 | self.is_t2i_azure = self._is_azure_openai(self.t2i_config["base_url"]) 47 | 48 | # Log detection results 49 | llm_provider = "[blue]Azure OpenAI[/blue]" if self.is_llm_azure else "[green]Standard OpenAI[/green]" 50 | t2i_provider = "[blue]Azure OpenAI[/blue]" if self.is_t2i_azure else "[green]Standard OpenAI[/green]" 51 | self.console.print(f"[cyan]📊 LLM Provider:[/cyan] {llm_provider}") 52 | self.console.print(f"[cyan]🎨 T2I Provider:[/cyan] {t2i_provider}") 53 | 54 | # Initialize clients 55 | self.console.print("[cyan]🔌 Initializing clients...[/cyan]") 56 | self.llm_client = self._initialize_llm_client() 57 | self.t2i_client = self._initialize_t2i_client() 58 | self.console.print("[green]✓ ModelClient initialization complete[/green]") 59 | 60 | def _is_azure_openai(self, base_url: str) -> bool: 61 | """ 62 | Check if the base URL is for Azure OpenAI 63 | 64 | Args: 65 | base_url: The base URL to check 66 | 67 | Returns: 68 | True if it's Azure OpenAI, False otherwise 69 | """ 70 | is_azure = ".openai.azure.com" in base_url.lower() 71 | self.console.print(f"[dim]🔍 Checking URL: {base_url}[/dim]") 72 | self.console.print(f"[dim] Contains '.openai.azure.com': {is_azure}[/dim]") 73 | return is_azure 74 | 75 | def _extract_azure_info(self, base_url: str) -> tuple[str, str, str]: 76 | """ 77 | Extract Azure OpenAI endpoint, deployment name and API version from base_url 78 | 79 | Args: 80 | base_url: Azure OpenAI URL like: 81 | https://resource.openai.azure.com/openai/deployments/model-name/...?api-version=2024-02-01 82 | 83 | Returns: 84 | Tuple of (azure_endpoint, deployment_name, api_version) 85 | """ 86 | import re 87 | from urllib.parse import urlparse, parse_qs 88 | 89 | # First extract the base endpoint and deployment from path 90 | endpoint_pattern = r'(https://[^/]+\.openai\.azure\.com)(?:/openai/deployments/([^/\?]+))?' 91 | match = re.match(endpoint_pattern, base_url) 92 | 93 | if match: 94 | azure_endpoint = match.group(1) 95 | deployment_name = match.group(2) if match.group(2) else "unknown" 96 | 97 | # Extract API version from query parameters 98 | parsed_url = urlparse(base_url) 99 | query_params = parse_qs(parsed_url.query) 100 | api_version = query_params.get('api-version', ['2024-02-01'])[0] 101 | 102 | self.console.print(f"[dim] Extracted endpoint: {azure_endpoint}[/dim]") 103 | self.console.print(f"[dim] Extracted deployment: {deployment_name}[/dim]") 104 | self.console.print(f"[dim] Extracted API version: {api_version}[/dim]") 105 | 106 | return azure_endpoint, deployment_name, api_version 107 | else: 108 | self.console.print(f"[yellow]⚠️ Could not parse Azure URL: {base_url}[/yellow]") 109 | # Fallback: assume the base_url is the endpoint 110 | return base_url, "unknown", "2024-02-01" 111 | 112 | def _initialize_llm_client(self) -> Union[OpenAI, AzureOpenAI]: 113 | """ 114 | Initialize LLM client (OpenAI or Azure OpenAI) 115 | 116 | Returns: 117 | Configured LLM client (OpenAI or AzureOpenAI) 118 | """ 119 | if self.is_llm_azure: 120 | self.console.print("[cyan]🔧 Initializing Azure OpenAI LLM client[/cyan]") 121 | # For Azure OpenAI, extract azure_endpoint, deployment and api_version from base_url 122 | base_url = self.llm_config["base_url"] 123 | azure_endpoint, deployment_name, api_version = self._extract_azure_info(base_url) 124 | 125 | # Store deployment name for use in API calls 126 | self.llm_deployment = deployment_name 127 | 128 | self.console.print(f"[dim] Azure Endpoint: {azure_endpoint}[/dim]") 129 | self.console.print(f"[dim] Deployment: {deployment_name}[/dim]") 130 | self.console.print(f"[dim] API Version: {api_version}[/dim]") 131 | 132 | client = AzureOpenAI( 133 | azure_endpoint=azure_endpoint, 134 | api_key=self.llm_config["api_key"], 135 | api_version=api_version, # Use extracted API version 136 | ) 137 | self.console.print("[green]✓ Azure OpenAI LLM client initialized[/green]") 138 | return client 139 | else: 140 | self.console.print("[cyan]🔧 Initializing standard OpenAI LLM client[/cyan]") 141 | self.console.print(f"[dim] Base URL: {self.llm_config['base_url']}[/dim]") 142 | 143 | client = OpenAI( 144 | base_url=self.llm_config["base_url"], 145 | api_key=self.llm_config["api_key"], 146 | ) 147 | self.console.print("[green]✓ Standard OpenAI LLM client initialized[/green]") 148 | return client 149 | 150 | def _initialize_t2i_client(self) -> Union[OpenAI, AzureOpenAI]: 151 | """ 152 | Initialize text-to-image client (OpenAI or Azure OpenAI) 153 | 154 | Returns: 155 | Configured text-to-image client (OpenAI or AzureOpenAI) 156 | """ 157 | if self.is_t2i_azure: 158 | self.console.print("[cyan]🔧 Initializing Azure OpenAI T2I client[/cyan]") 159 | # For Azure OpenAI, extract azure_endpoint, deployment and api_version from base_url 160 | base_url = self.t2i_config["base_url"] 161 | azure_endpoint, deployment_name, api_version = self._extract_azure_info(base_url) 162 | 163 | # Store deployment name for use in API calls 164 | self.t2i_deployment = deployment_name 165 | 166 | self.console.print(f"[dim] Azure Endpoint: {azure_endpoint}[/dim]") 167 | self.console.print(f"[dim] Deployment: {deployment_name}[/dim]") 168 | self.console.print(f"[dim] API Version: {api_version}[/dim]") 169 | 170 | client = AzureOpenAI( 171 | azure_endpoint=azure_endpoint, 172 | api_key=self.t2i_config["api_key"], 173 | api_version=api_version, # Use extracted API version 174 | ) 175 | self.console.print("[green]✓ Azure OpenAI T2I client initialized[/green]") 176 | return client 177 | else: 178 | self.console.print("[cyan]🔧 Initializing standard OpenAI T2I client[/cyan]") 179 | self.console.print(f"[dim] Base URL: {self.t2i_config['base_url']}[/dim]") 180 | 181 | client = OpenAI( 182 | base_url=self.t2i_config["base_url"], 183 | api_key=self.t2i_config["api_key"], 184 | ) 185 | self.console.print("[green]✓ Standard OpenAI T2I client initialized[/green]") 186 | return client 187 | 188 | def get_answer(self, question: str, model: Optional[str] = None, max_retries: int = 3) -> str: 189 | """ 190 | Get answer using LLM (with retry mechanism) 191 | 192 | Args: 193 | question: User question 194 | model: Specify model to use, if not specified use default model from config 195 | max_retries: Maximum retry attempts 196 | 197 | Returns: 198 | LLM answer 199 | """ 200 | # For Azure OpenAI, use deployment name; for others, use model name 201 | if self.is_llm_azure and hasattr(self, 'llm_deployment'): 202 | model_name = self.llm_deployment 203 | specified_model = model or self.llm_config["model_name"] 204 | self.console.print(f"[cyan]🤖 Making Azure OpenAI LLM request[/cyan]") 205 | self.console.print(f"[dim] Using deployment: {model_name}[/dim]") 206 | self.console.print(f"[dim] Requested model: {specified_model}[/dim]") 207 | else: 208 | model_name = model or self.llm_config["model_name"] 209 | self.console.print(f"[cyan]🤖 Making LLM request[/cyan]") 210 | self.console.print(f"[dim] Model: {model_name}[/dim]") 211 | 212 | provider = 'Azure OpenAI' if self.is_llm_azure else 'OpenAI' 213 | self.console.print(f"[dim] Provider: {provider}[/dim]") 214 | self.console.print(f"[dim] Max retries: {max_retries}[/dim]") 215 | 216 | for attempt in range(max_retries): 217 | try: 218 | response = self.llm_client.chat.completions.create( 219 | model=model_name, 220 | messages=[ 221 | {"role": "user", "content": question} 222 | ], 223 | max_tokens=self.max_tokens, 224 | temperature=self.temperature, 225 | timeout=60 226 | ) 227 | 228 | answer = response.choices[0].message.content 229 | return answer 230 | 231 | except Exception as e: 232 | error_msg = str(e) 233 | self.console.print(f"[red]LLM request error (attempt {attempt + 1}/{max_retries}): {error_msg}[/red]") 234 | 235 | # Provide detailed error information 236 | self.console.print(f"[yellow]Model used: {model_name}[/yellow]") 237 | self.console.print(f"[yellow]Base URL: {self.llm_config.get('base_url', 'Unknown')}[/yellow]") 238 | 239 | # If this is the last attempt, raise exception 240 | if attempt == max_retries - 1: 241 | self.console.print(f"[red]All retry attempts failed, giving up request[/red]") 242 | raise Exception(f"LLM request failed after {max_retries} retries: {error_msg}") 243 | else: 244 | # Exponential backoff delay 245 | delay = 2 ** attempt 246 | self.console.print(f"[yellow]Waiting {delay} seconds before retry...[/yellow]") 247 | time.sleep(delay) 248 | 249 | def generate_text(self, prompt: str, model: Optional[str] = None) -> str: 250 | """ 251 | Generate text using LLM (alias for get_answer) 252 | 253 | Args: 254 | prompt: Text prompt 255 | model: Specify model to use, if not specified use default model from config 256 | 257 | Returns: 258 | Generated text 259 | """ 260 | return self.get_answer(prompt, model) 261 | 262 | def get_image(self, prompt: str, model: Optional[str] = None) -> Dict[str, Union[str, bytes, None]]: 263 | """ 264 | Generate image using text-to-image model 265 | 266 | Args: 267 | prompt: Image description prompt 268 | model: Specify model to use, if not specified use default model from config 269 | 270 | Returns: 271 | Dictionary containing url and content: {"url": str, "content": bytes} 272 | """ 273 | try: 274 | # For Azure OpenAI, use deployment name; for others, use model name 275 | if self.is_t2i_azure and hasattr(self, 't2i_deployment'): 276 | model_name = self.t2i_deployment 277 | specified_model = model or self.t2i_config["model_name"] 278 | self.console.print(f"[cyan]🎨 Making Azure OpenAI T2I request[/cyan]") 279 | self.console.print(f"[dim] Using deployment: {model_name}[/dim]") 280 | self.console.print(f"[dim] Requested model: {specified_model}[/dim]") 281 | else: 282 | model_name = model or self.t2i_config["model_name"] 283 | self.console.print(f"[cyan]🎨 Making T2I request[/cyan]") 284 | self.console.print(f"[dim] Model: {model_name}[/dim]") 285 | 286 | provider = 'Azure OpenAI' if self.is_t2i_azure else 'OpenAI' 287 | self.console.print(f"[dim] Provider: {provider}[/dim]") 288 | self.console.print(f"[dim] Image size: {self.image_size}[/dim]") 289 | self.console.print(f"[dim] Quality: {self.quality}[/dim]") 290 | 291 | # Generate image request parameters - start with basic params 292 | generate_params = { 293 | "model": model_name, 294 | "prompt": prompt, 295 | "n": 1 296 | } 297 | 298 | # Add size and quality parameters based on provider type 299 | if self.is_t2i_azure: 300 | self.console.print("[cyan]🔧 Configuring for Azure OpenAI[/cyan]") 301 | # For Azure OpenAI, use basic parameters 302 | generate_params["size"] = self.image_size 303 | # Azure OpenAI may support quality parameter for DALL-E models 304 | deployment_model = specified_model if self.is_t2i_azure and hasattr(self, 't2i_deployment') else model_name 305 | if deployment_model.startswith("dall-e"): 306 | generate_params["quality"] = self.quality 307 | self.console.print("[dim] Added quality parameter for DALL-E[/dim]") 308 | 309 | else: 310 | self.console.print("[cyan]🔧 Configuring for standard OpenAI[/cyan]") 311 | # For OpenAI and other OpenAI-compatible APIs 312 | base_url = self.t2i_config.get("base_url", "") 313 | 314 | if "openai.com" in base_url or model_name.startswith("dall-e"): 315 | generate_params["size"] = self.image_size 316 | # Add quality parameter only for dall-e models 317 | if model_name.startswith("dall-e"): 318 | generate_params["quality"] = self.quality 319 | self.console.print("[dim] Added quality parameter for DALL-E[/dim]") 320 | else: 321 | # For other providers (like Doubao/ByteDance), use basic parameters 322 | generate_params["size"] = self.image_size 323 | self.console.print("[dim] Using basic parameters for other provider[/dim]") 324 | 325 | # Don't add quality parameter for non-OpenAI providers 326 | # as it may cause "InvalidParameter" errors 327 | 328 | self.console.print(f"[cyan]📤 Sending request with parameters:[/cyan]") 329 | for key, value in generate_params.items(): 330 | self.console.print(f"[dim] {key}: {value}[/dim]") 331 | 332 | response = self.t2i_client.images.generate(**generate_params) 333 | 334 | self.console.print("[green]✓ Image generation request successful[/green]") 335 | 336 | image_url = response.data[0].url 337 | self.console.print(f"[green]✓ Image URL received: {image_url}[/green]") 338 | 339 | # Download image content with retry mechanism 340 | self.console.print("[cyan]⬇️ Downloading image content...[/cyan]") 341 | image_content = self._download_image_with_retry(image_url, max_retries=3) 342 | 343 | if image_content: 344 | size_mb = len(image_content) / (1024 * 1024) 345 | self.console.print(f"[green]✓ Download successful: {len(image_content)} bytes ({size_mb:.2f} MB)[/green]") 346 | else: 347 | self.console.print("[yellow]⚠️ Image download failed, but URL is available[/yellow]") 348 | 349 | return { 350 | "url": image_url, 351 | "content": image_content 352 | } 353 | 354 | except Exception as e: 355 | self.console.print(f"[red]❌ Image generation failed: {e}[/red]") 356 | # Provide helpful error information 357 | self.console.print(f"[yellow]🔍 Debug information:[/yellow]") 358 | self.console.print(f"[dim] Model used: {model_name}[/dim]") 359 | self.console.print(f"[dim] Base URL: {self.t2i_config.get('base_url', 'Unknown')}[/dim]") 360 | self.console.print(f"[dim] Is Azure OpenAI: {self.is_t2i_azure}[/dim]") 361 | self.console.print(f"[dim] Error type: {type(e).__name__}[/dim]") 362 | raise 363 | 364 | def _download_image_with_retry(self, image_url: str, max_retries: int = 3) -> Optional[bytes]: 365 | """ 366 | Download image with retry mechanism 367 | 368 | Args: 369 | image_url: Image URL 370 | max_retries: Maximum retry attempts 371 | 372 | Returns: 373 | Image content bytes, returns None if failed 374 | """ 375 | import time 376 | import ssl 377 | 378 | for attempt in range(max_retries): 379 | try: 380 | self.console.print(f"[dim]📥 Download attempt {attempt + 1}/{max_retries}[/dim]") 381 | 382 | # Set request parameters with SSL tolerance 383 | session = requests.Session() 384 | session.verify = True # Verify SSL certificate 385 | 386 | # Add User-Agent and other headers 387 | headers = { 388 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 389 | 'Accept': 'image/*,*/*;q=0.8', 390 | 'Accept-Encoding': 'gzip, deflate, br', 391 | 'Connection': 'keep-alive' 392 | } 393 | 394 | response = session.get( 395 | image_url, 396 | timeout=60, # Increase timeout 397 | headers=headers, 398 | stream=True # Stream download 399 | ) 400 | response.raise_for_status() 401 | 402 | # Get image content 403 | image_content = response.content 404 | self.console.print(f"Image downloaded successfully, size: {len(image_content)} bytes") 405 | return image_content 406 | 407 | except (requests.exceptions.SSLError, ssl.SSLError) as ssl_error: 408 | self.console.print(f"SSL error (attempt {attempt + 1}/{max_retries}): {str(ssl_error)}") 409 | if attempt == max_retries - 1: 410 | self.console.print("SSL connection failed consistently, possibly server certificate issue") 411 | else: 412 | time.sleep(2 ** attempt) # Exponential backoff 413 | 414 | except requests.exceptions.ConnectionError as conn_error: 415 | self.console.print(f"Connection error (attempt {attempt + 1}/{max_retries}): {str(conn_error)}") 416 | if attempt == max_retries - 1: 417 | self.console.print("Network connection failed consistently") 418 | else: 419 | time.sleep(2 ** attempt) 420 | 421 | except requests.exceptions.Timeout as timeout_error: 422 | self.console.print(f"Timeout error (attempt {attempt + 1}/{max_retries}): {str(timeout_error)}") 423 | if attempt == max_retries - 1: 424 | self.console.print("Request timeout") 425 | else: 426 | time.sleep(1) 427 | 428 | except Exception as e: 429 | self.console.print(f"Failed to download image (attempt {attempt + 1}/{max_retries}): {str(e)}") 430 | if attempt == max_retries - 1: 431 | self.console.print("All retry attempts failed") 432 | else: 433 | time.sleep(1) 434 | 435 | return None 436 | 437 | def get_current_settings(self) -> dict: 438 | """ 439 | Get current settings information 440 | 441 | Returns: 442 | Current settings dictionary 443 | """ 444 | return { 445 | "llm_base_url": self.llm_config["base_url"], 446 | "llm_model_name": self.llm_config["model_name"], 447 | "llm_is_azure": self.is_llm_azure, 448 | "t2i_base_url": self.t2i_config["base_url"], 449 | "t2i_model_name": self.t2i_config["model_name"], 450 | "t2i_is_azure": self.is_t2i_azure, 451 | "max_tokens": self.max_tokens, 452 | "temperature": self.temperature, 453 | "image_size": self.image_size, 454 | "quality": self.quality 455 | } 456 | 457 | 458 | def main(): 459 | """Main function demonstrating model client usage""" 460 | console = Console() 461 | try: 462 | # Create model client instance 463 | client = ModelClient() 464 | 465 | # Display current configuration information 466 | console.print("=== Current Configuration ===") 467 | settings = client.get_current_settings() 468 | for key, value in settings.items(): 469 | console.print(f"{key}: {value}") 470 | console.print() 471 | 472 | # Test LLM Q&A functionality 473 | console.print("=== LLM Q&A Test ===") 474 | question = "What is artificial intelligence? Please answer briefly in 50 words or less." 475 | answer = client.get_answer(question) 476 | console.print(f"Question: {question}") 477 | console.print(f"Answer: {answer}") 478 | console.print() 479 | 480 | # Test text-to-image functionality 481 | console.print("=== Text-to-Image Test ===") 482 | image_prompt = "A cute cat playing in a garden, cartoon style" 483 | image_result = client.get_image(image_prompt) 484 | console.print(f"Image description: {image_prompt}") 485 | 486 | if "error" in image_result: 487 | console.print(f"Generation failed: {image_result['error']}") 488 | else: 489 | console.print(f"Generated image URL: {image_result['url']}") 490 | if image_result['content']: 491 | content_size = len(image_result['content']) 492 | console.print(f"Image content size: {content_size} bytes") 493 | # Option to save image locally 494 | # with open("generated_image.png", "wb") as f: 495 | # f.write(image_result['content']) 496 | # console.print("Image saved as generated_image.png") 497 | else: 498 | console.print("Image content download failed") 499 | console.print() 500 | 501 | console.print("=== Program completed ===") 502 | 503 | except Exception as e: 504 | console.print(f"Program error: {str(e)}") 505 | 506 | 507 | if __name__ == "__main__": 508 | main() --------------------------------------------------------------------------------